mirror of
https://github.com/samsonjs/SJSAssetExportSession.git
synced 2026-04-27 14:57:46 +00:00
Add a basic exporter
This doesn't have a great API yet and hasn't been tested so it may not even work.
This commit is contained in:
parent
6d464ff2b8
commit
36f055d36f
3 changed files with 323 additions and 6 deletions
|
|
@ -7,10 +7,12 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
7B7AE3092C36615700DB7391 /* SampleWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7AE3082C36615700DB7391 /* SampleWriter.swift */; };
|
||||||
7B9BC00E2C305D2C00C160C2 /* SJSAssetExportSession.docc in Sources */ = {isa = PBXBuildFile; fileRef = 7B9BC00D2C305D2C00C160C2 /* SJSAssetExportSession.docc */; };
|
7B9BC00E2C305D2C00C160C2 /* SJSAssetExportSession.docc in Sources */ = {isa = PBXBuildFile; fileRef = 7B9BC00D2C305D2C00C160C2 /* SJSAssetExportSession.docc */; };
|
||||||
7B9BC0142C305D2C00C160C2 /* SJSAssetExportSession.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B9BC0092C305D2C00C160C2 /* SJSAssetExportSession.framework */; };
|
7B9BC0142C305D2C00C160C2 /* SJSAssetExportSession.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B9BC0092C305D2C00C160C2 /* SJSAssetExportSession.framework */; };
|
||||||
7B9BC0192C305D2C00C160C2 /* SJSAssetExportSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9BC0182C305D2C00C160C2 /* SJSAssetExportSessionTests.swift */; };
|
7B9BC0192C305D2C00C160C2 /* SJSAssetExportSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9BC0182C305D2C00C160C2 /* SJSAssetExportSessionTests.swift */; };
|
||||||
7B9BC01A2C305D2C00C160C2 /* SJSAssetExportSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B9BC00C2C305D2C00C160C2 /* SJSAssetExportSession.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
7B9BC01A2C305D2C00C160C2 /* SJSAssetExportSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B9BC00C2C305D2C00C160C2 /* SJSAssetExportSession.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
7B9BC0282C30612C00C160C2 /* ExportSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9BC0272C30612C00C160C2 /* ExportSession.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
|
@ -24,11 +26,13 @@
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
7B7AE3082C36615700DB7391 /* SampleWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleWriter.swift; sourceTree = "<group>"; };
|
||||||
7B9BC0092C305D2C00C160C2 /* SJSAssetExportSession.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SJSAssetExportSession.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
7B9BC0092C305D2C00C160C2 /* SJSAssetExportSession.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SJSAssetExportSession.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
7B9BC00C2C305D2C00C160C2 /* SJSAssetExportSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SJSAssetExportSession.h; sourceTree = "<group>"; };
|
7B9BC00C2C305D2C00C160C2 /* SJSAssetExportSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SJSAssetExportSession.h; sourceTree = "<group>"; };
|
||||||
7B9BC00D2C305D2C00C160C2 /* SJSAssetExportSession.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = SJSAssetExportSession.docc; sourceTree = "<group>"; };
|
7B9BC00D2C305D2C00C160C2 /* SJSAssetExportSession.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = SJSAssetExportSession.docc; sourceTree = "<group>"; };
|
||||||
7B9BC0132C305D2C00C160C2 /* SJSAssetExportSessionTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SJSAssetExportSessionTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
7B9BC0132C305D2C00C160C2 /* SJSAssetExportSessionTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SJSAssetExportSessionTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
7B9BC0182C305D2C00C160C2 /* SJSAssetExportSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SJSAssetExportSessionTests.swift; sourceTree = "<group>"; };
|
7B9BC0182C305D2C00C160C2 /* SJSAssetExportSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SJSAssetExportSessionTests.swift; sourceTree = "<group>"; };
|
||||||
|
7B9BC0272C30612C00C160C2 /* ExportSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSession.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -73,6 +77,8 @@
|
||||||
children = (
|
children = (
|
||||||
7B9BC00C2C305D2C00C160C2 /* SJSAssetExportSession.h */,
|
7B9BC00C2C305D2C00C160C2 /* SJSAssetExportSession.h */,
|
||||||
7B9BC00D2C305D2C00C160C2 /* SJSAssetExportSession.docc */,
|
7B9BC00D2C305D2C00C160C2 /* SJSAssetExportSession.docc */,
|
||||||
|
7B9BC0272C30612C00C160C2 /* ExportSession.swift */,
|
||||||
|
7B7AE3082C36615700DB7391 /* SampleWriter.swift */,
|
||||||
);
|
);
|
||||||
path = SJSAssetExportSession;
|
path = SJSAssetExportSession;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -194,6 +200,8 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
7B7AE3092C36615700DB7391 /* SampleWriter.swift in Sources */,
|
||||||
|
7B9BC0282C30612C00C160C2 /* ExportSession.swift in Sources */,
|
||||||
7B9BC00E2C305D2C00C160C2 /* SJSAssetExportSession.docc in Sources */,
|
7B9BC00E2C305D2C00C160C2 /* SJSAssetExportSession.docc in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
@ -276,6 +284,7 @@
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
VERSION_INFO_PREFIX = "";
|
VERSION_INFO_PREFIX = "";
|
||||||
};
|
};
|
||||||
|
|
@ -332,6 +341,7 @@
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
VERSION_INFO_PREFIX = "";
|
VERSION_INFO_PREFIX = "";
|
||||||
};
|
};
|
||||||
|
|
@ -362,7 +372,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
"@loader_path/Frameworks",
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
|
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
|
||||||
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
|
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
|
||||||
|
|
@ -373,7 +383,6 @@
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_INSTALL_OBJC_HEADER = NO;
|
SWIFT_INSTALL_OBJC_HEADER = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
XROS_DEPLOYMENT_TARGET = 2.0;
|
XROS_DEPLOYMENT_TARGET = 2.0;
|
||||||
};
|
};
|
||||||
|
|
@ -404,7 +413,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
"@loader_path/Frameworks",
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
|
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
|
||||||
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
|
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
|
||||||
|
|
@ -415,7 +424,6 @@
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_INSTALL_OBJC_HEADER = NO;
|
SWIFT_INSTALL_OBJC_HEADER = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
XROS_DEPLOYMENT_TARGET = 2.0;
|
XROS_DEPLOYMENT_TARGET = 2.0;
|
||||||
};
|
};
|
||||||
|
|
@ -437,7 +445,6 @@
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
XROS_DEPLOYMENT_TARGET = 2.0;
|
XROS_DEPLOYMENT_TARGET = 2.0;
|
||||||
};
|
};
|
||||||
|
|
@ -459,7 +466,6 @@
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,7";
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
XROS_DEPLOYMENT_TARGET = 2.0;
|
XROS_DEPLOYMENT_TARGET = 2.0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
52
SJSAssetExportSession/ExportSession.swift
Normal file
52
SJSAssetExportSession/ExportSession.swift
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
//
|
||||||
|
// ExportSession.swift
|
||||||
|
// SJSAssetExportSession
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2024-06-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
public import AVFoundation
|
||||||
|
|
||||||
|
public final class ExportSession {
|
||||||
|
public enum Error: LocalizedError {
|
||||||
|
case setupFailure(reason: String)
|
||||||
|
case readFailure((any Swift.Error)?)
|
||||||
|
case writeFailure((any Swift.Error)?)
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case let .setupFailure(reason):
|
||||||
|
reason
|
||||||
|
case let .readFailure(underlyingError):
|
||||||
|
underlyingError?.localizedDescription ?? "Unknown read failure"
|
||||||
|
case let .writeFailure(underlyingError):
|
||||||
|
underlyingError?.localizedDescription ?? "Unknown write failure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func export(
|
||||||
|
asset: sending AVAsset,
|
||||||
|
audioMix: sending AVAudioMix?,
|
||||||
|
audioOutputSettings: [String: (any Sendable)],
|
||||||
|
videoComposition: sending AVVideoComposition?,
|
||||||
|
videoOutputSettings: [String: (any Sendable)],
|
||||||
|
timeRange: CMTimeRange? = nil,
|
||||||
|
optimizeForNetworkUse: Bool = false,
|
||||||
|
to outputURL: URL,
|
||||||
|
as fileType: AVFileType
|
||||||
|
) async throws {
|
||||||
|
let sampleWriter = try await SampleWriter(
|
||||||
|
asset: asset,
|
||||||
|
timeRange: timeRange ?? CMTimeRange(start: .zero, duration: .positiveInfinity),
|
||||||
|
audioMix: audioMix,
|
||||||
|
audioOutputSettings: audioOutputSettings,
|
||||||
|
videoComposition: videoComposition,
|
||||||
|
videoOutputSettings: videoOutputSettings,
|
||||||
|
optimizeForNetworkUse: optimizeForNetworkUse,
|
||||||
|
outputURL: outputURL,
|
||||||
|
fileType: fileType
|
||||||
|
)
|
||||||
|
try await sampleWriter.writeSamples()
|
||||||
|
}
|
||||||
|
}
|
||||||
259
SJSAssetExportSession/SampleWriter.swift
Normal file
259
SJSAssetExportSession/SampleWriter.swift
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
//
|
||||||
|
// SampleWriter.swift
|
||||||
|
// SJSAssetExportSession
|
||||||
|
//
|
||||||
|
// Created by Sami Samhuri on 2024-07-03.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVFoundation.AVAsset
|
||||||
|
|
||||||
|
private extension AVAsset {
|
||||||
|
func sendTracks(withMediaType mediaType: AVMediaType) async throws -> sending [AVAssetTrack] {
|
||||||
|
try await loadTracks(withMediaType: mediaType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actor SampleWriter {
|
||||||
|
private let queue = DispatchSerialQueue(
|
||||||
|
label: "SJSAssetExportSession.SampleWriter",
|
||||||
|
autoreleaseFrequency: .workItem,
|
||||||
|
target: .global()
|
||||||
|
)
|
||||||
|
|
||||||
|
public nonisolated var unownedExecutor: UnownedSerialExecutor {
|
||||||
|
queue.asUnownedSerialExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
let audioTracks: [AVAssetTrack]
|
||||||
|
|
||||||
|
let audioMix: AVAudioMix?
|
||||||
|
|
||||||
|
let audioOutputSettings: [String: (any Sendable)]
|
||||||
|
|
||||||
|
let videoTracks: [AVAssetTrack]
|
||||||
|
|
||||||
|
let videoComposition: AVVideoComposition?
|
||||||
|
|
||||||
|
let videoOutputSettings: [String: (any Sendable)]
|
||||||
|
|
||||||
|
let reader: AVAssetReader
|
||||||
|
|
||||||
|
let writer: AVAssetWriter
|
||||||
|
|
||||||
|
let duration: CMTime
|
||||||
|
|
||||||
|
let timeRange: CMTimeRange
|
||||||
|
|
||||||
|
private var audioOutput: AVAssetReaderAudioMixOutput?
|
||||||
|
|
||||||
|
private var audioInput: AVAssetWriterInput?
|
||||||
|
|
||||||
|
private var videoOutput: AVAssetReaderVideoCompositionOutput?
|
||||||
|
|
||||||
|
private var videoInput: AVAssetWriterInput?
|
||||||
|
|
||||||
|
private var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor?
|
||||||
|
|
||||||
|
init(
|
||||||
|
asset: sending AVAsset,
|
||||||
|
timeRange: CMTimeRange,
|
||||||
|
audioMix: AVAudioMix?,
|
||||||
|
audioOutputSettings: sending [String: (any Sendable)],
|
||||||
|
videoComposition: AVVideoComposition?,
|
||||||
|
videoOutputSettings: sending [String: (any Sendable)],
|
||||||
|
optimizeForNetworkUse: Bool,
|
||||||
|
outputURL: URL,
|
||||||
|
fileType: AVFileType
|
||||||
|
) async throws {
|
||||||
|
let duration =
|
||||||
|
if timeRange.duration.isValid && !timeRange.duration.isPositiveInfinity {
|
||||||
|
timeRange.duration
|
||||||
|
} else {
|
||||||
|
try await asset.load(.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
let reader = try AVAssetReader(asset: asset)
|
||||||
|
reader.timeRange = timeRange
|
||||||
|
|
||||||
|
let writer = try AVAssetWriter(outputURL: outputURL, fileType: fileType)
|
||||||
|
writer.shouldOptimizeForNetworkUse = optimizeForNetworkUse
|
||||||
|
guard writer.canApply(outputSettings: videoOutputSettings, forMediaType: .video) else {
|
||||||
|
throw ExportSession.Error.setupFailure(reason: "Cannot apply video output settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.audioTracks = try await asset.sendTracks(withMediaType: .audio)
|
||||||
|
self.audioMix = audioMix
|
||||||
|
self.audioOutputSettings = audioOutputSettings
|
||||||
|
self.videoTracks = try await asset.sendTracks(withMediaType: .video)
|
||||||
|
self.videoComposition = videoComposition
|
||||||
|
self.videoOutputSettings = videoOutputSettings
|
||||||
|
self.reader = reader
|
||||||
|
self.writer = writer
|
||||||
|
self.duration = duration
|
||||||
|
self.timeRange = timeRange
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSamples() async throws {
|
||||||
|
writer.startWriting()
|
||||||
|
reader.startReading()
|
||||||
|
writer.startSession(atSourceTime: timeRange.start)
|
||||||
|
|
||||||
|
async let audioResult = try encodeAudioTracks(audioTracks)
|
||||||
|
async let videoResult = try encodeVideoTracks(videoTracks)
|
||||||
|
_ = try await (audioResult, videoResult)
|
||||||
|
|
||||||
|
if reader.status == .cancelled || writer.status == .cancelled {
|
||||||
|
throw CancellationError()
|
||||||
|
} else if writer.status == .failed {
|
||||||
|
reader.cancelReading()
|
||||||
|
throw ExportSession.Error.writeFailure(writer.error)
|
||||||
|
} else if reader.status == .failed {
|
||||||
|
writer.cancelWriting()
|
||||||
|
throw ExportSession.Error.readFailure(reader.error)
|
||||||
|
} else {
|
||||||
|
await withCheckedContinuation { continuation in
|
||||||
|
writer.finishWriting {
|
||||||
|
continuation.resume(returning: ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func encodeAudioTracks(_ audioTracks: [AVAssetTrack]) async throws -> Bool {
|
||||||
|
guard !audioTracks.isEmpty else { return false }
|
||||||
|
|
||||||
|
let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil)
|
||||||
|
guard reader.canAdd(audioOutput) else {
|
||||||
|
throw ExportSession.Error.setupFailure(reason: "Can't add audio output to reader")
|
||||||
|
}
|
||||||
|
reader.add(audioOutput)
|
||||||
|
self.audioOutput = audioOutput
|
||||||
|
|
||||||
|
let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioOutputSettings)
|
||||||
|
guard writer.canAdd(audioInput) else {
|
||||||
|
throw ExportSession.Error.setupFailure(reason: "Can't add audio input to writer")
|
||||||
|
}
|
||||||
|
writer.add(audioInput)
|
||||||
|
self.audioInput = audioInput
|
||||||
|
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
self.audioInput?.requestMediaDataWhenReady(on: queue) {
|
||||||
|
let hasMoreSamples = self.assumeIsolated { $0.writeReadyAudioSamples() }
|
||||||
|
if !hasMoreSamples {
|
||||||
|
continuation.resume(returning: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeReadyAudioSamples() -> Bool {
|
||||||
|
guard let audioOutput, let audioInput else { return true }
|
||||||
|
|
||||||
|
while audioInput.isReadyForMoreMediaData {
|
||||||
|
guard reader.status == .reading && writer.status == .writing,
|
||||||
|
let sampleBuffer = audioOutput.copyNextSampleBuffer() else {
|
||||||
|
audioInput.markAsFinished()
|
||||||
|
NSLog("Finished encoding ready audio samples from \(audioOutput)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard audioInput.append(sampleBuffer) else {
|
||||||
|
NSLog("Failed to append audio sample buffer \(sampleBuffer) to input \(audioInput)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything was appended successfully, return true indicating there's more to do.
|
||||||
|
NSLog("Completed encoding ready audio samples, more to come...")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func encodeVideoTracks(_ videoTracks: [AVAssetTrack]) async throws -> Bool {
|
||||||
|
guard !videoTracks.isEmpty else { return false }
|
||||||
|
|
||||||
|
guard let width = videoComposition.map({ Int($0.renderSize.width) })
|
||||||
|
?? (videoOutputSettings[AVVideoWidthKey] as? NSNumber)?.intValue,
|
||||||
|
let height = videoComposition.map({ Int($0.renderSize.height) })
|
||||||
|
?? (videoOutputSettings[AVVideoHeightKey] as? NSNumber)?.intValue else {
|
||||||
|
throw ExportSession.Error.setupFailure(reason: "Export dimensions must be provided in a video composition or video output settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoOutput = AVAssetReaderVideoCompositionOutput(videoTracks: videoTracks, videoSettings: nil)
|
||||||
|
videoOutput.alwaysCopiesSampleData = false
|
||||||
|
videoOutput.videoComposition = videoComposition
|
||||||
|
guard reader.canAdd(videoOutput) else {
|
||||||
|
throw ExportSession.Error.setupFailure(reason: "Can't add video output to reader")
|
||||||
|
}
|
||||||
|
reader.add(videoOutput)
|
||||||
|
self.videoOutput = videoOutput
|
||||||
|
|
||||||
|
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoOutputSettings)
|
||||||
|
guard writer.canAdd(videoInput) else {
|
||||||
|
throw ExportSession.Error.setupFailure(reason: "Can't add video input to writer")
|
||||||
|
}
|
||||||
|
writer.add(videoInput)
|
||||||
|
self.videoInput = videoInput
|
||||||
|
|
||||||
|
let pixelBufferAttributes: [String: Any] = [
|
||||||
|
kCVPixelBufferPixelFormatTypeKey as String: NSNumber(integerLiteral: Int(kCVPixelFormatType_32RGBA)),
|
||||||
|
kCVPixelBufferWidthKey as String: NSNumber(integerLiteral: width),
|
||||||
|
kCVPixelBufferHeightKey as String: NSNumber(integerLiteral: height),
|
||||||
|
"IOSurfaceOpenGLESTextureCompatibility": NSNumber(booleanLiteral: true),
|
||||||
|
"IOSurfaceOpenGLESFBOCompatibility": NSNumber(booleanLiteral: true),
|
||||||
|
]
|
||||||
|
pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(
|
||||||
|
assetWriterInput: videoInput,
|
||||||
|
sourcePixelBufferAttributes: pixelBufferAttributes
|
||||||
|
)
|
||||||
|
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
self.videoInput?.requestMediaDataWhenReady(on: queue) {
|
||||||
|
let hasMoreSamples = self.assumeIsolated { $0.writeReadyVideoSamples() }
|
||||||
|
if !hasMoreSamples {
|
||||||
|
continuation.resume(returning: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeReadyVideoSamples() -> Bool {
|
||||||
|
guard let videoOutput, let videoInput, let pixelBufferAdaptor else { return true }
|
||||||
|
|
||||||
|
while videoInput.isReadyForMoreMediaData {
|
||||||
|
guard reader.status == .reading && writer.status == .writing,
|
||||||
|
let sampleBuffer = videoOutput.copyNextSampleBuffer() else {
|
||||||
|
videoInput.markAsFinished()
|
||||||
|
NSLog("Finished encoding ready video samples from \(videoOutput)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let samplePresentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) - timeRange.start
|
||||||
|
let progress = Float(samplePresentationTime.seconds / duration.seconds)
|
||||||
|
#warning("TODO: publish progress to an AsyncStream")
|
||||||
|
|
||||||
|
guard let pixelBufferPool = pixelBufferAdaptor.pixelBufferPool else {
|
||||||
|
NSLog("No pixel buffer pool available on adaptor \(pixelBufferAdaptor)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var toRenderBuffer: CVPixelBuffer?
|
||||||
|
let result = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &toRenderBuffer)
|
||||||
|
var handled = false
|
||||||
|
if result == kCVReturnSuccess, let toBuffer = toRenderBuffer {
|
||||||
|
handled = pixelBufferAdaptor.append(toBuffer, withPresentationTime: samplePresentationTime)
|
||||||
|
if !handled { return false }
|
||||||
|
}
|
||||||
|
if !handled {
|
||||||
|
#warning("is this really necessary?! seems like a failure scenario...")
|
||||||
|
guard videoInput.append(sampleBuffer) else {
|
||||||
|
NSLog("Failed to append video sample buffer \(sampleBuffer) to input \(videoInput)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything was appended successfully, return true indicating there's more to do.
|
||||||
|
NSLog("Completed encoding ready video samples, more to come...")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue