avsubtitleswriter/main.m
/* |
File: main.m |
Abstract: This implementation file covers writing out subtitles to a new movie file. This is accomplished with AVAssetReader, AVAssetWriter, and the SubtitlesTextReader. Steps are taken to preserve much of the source movies tracks and metadata in the new movie it writes out. |
Version: 1.0.1 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2013 Apple Inc. All Rights Reserved. |
*/ |
#import <Foundation/Foundation.h> |
#import <AVFoundation/AVFoundation.h> |
#import <getopt.h> |
#import "SubtitlesTextReader.h" |
void writeSubtitles(NSString *inputPath, NSString *outputPath, NSArray *subtitlesTextPaths); |
int main(int argc, char * const *argv) |
{ |
@autoreleasepool |
{ |
NSString *inputPath; |
NSString *outputPath; |
NSMutableArray *subtitlesTextPaths = [NSMutableArray array]; |
int option = -1; |
static struct option longopts[] = |
{ |
{"input", required_argument, NULL, 'i'}, |
{"output", required_argument, NULL, 'o'}, |
{"subtitles", required_argument, NULL, 's'}, |
{0, 0, 0, 0} |
}; |
const char *shortopts = "i:o:s:"; |
while ((option = getopt_long(argc, argv, shortopts, longopts, NULL)) != -1) |
{ |
switch (option) |
{ |
case 'i': |
{ |
inputPath = [NSString stringWithCString:optarg encoding:NSMacOSRomanStringEncoding]; |
break; |
} |
case 'o': |
{ |
outputPath = [NSString stringWithCString:optarg encoding:NSMacOSRomanStringEncoding]; |
break; |
} |
case 's': |
{ |
[subtitlesTextPaths addObject:[NSString stringWithCString:optarg encoding:NSMacOSRomanStringEncoding]]; |
break; |
} |
} |
} |
if (inputPath && outputPath) |
{ |
writeSubtitles(inputPath, outputPath, subtitlesTextPaths); |
} |
else |
{ |
printf("Usage: subtitleswriter -i [input_file] -o [output_file] -s [subtitles_file]\n"); |
printf(" Creates a new movie file at the specified output location, with audio, video, and subtitles from the input source, adding subtitles from the provide subtitles file(s). Each subtitles file will become a subtitle track in the output movie.\n"); |
} |
} |
return 0; |
} |
void writeSubtitles(NSString *inputPath, NSString *outputPath, NSArray *subtitlesTextPaths) |
{ |
NSError *error; |
// Setup the asset reader and writer |
AVAsset *asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:[inputPath stringByExpandingTildeInPath]]]; |
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:asset error:&error]; |
if (error) |
{ |
NSLog(@"error creating asset reader. exiting: %@", error); |
return; |
} |
NSURL *outputURL = [NSURL fileURLWithPath:[outputPath stringByExpandingTildeInPath]]; |
AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&error]; |
if (error) |
{ |
NSLog(@"error creating asset writer. exiting: %@", error); |
return; |
} |
// Copy metadata from the asset to the asset writer |
NSMutableArray *assetMetadata = [NSMutableArray array]; |
for (NSString *metadataFormat in asset.availableMetadataFormats) |
[assetMetadata addObjectsFromArray:[asset metadataForFormat:metadataFormat]]; |
assetWriter.metadata = assetMetadata; |
assetWriter.shouldOptimizeForNetworkUse = YES; |
// Build up inputs and outputs for the reader and writer to carry over the tracks from the input movie into the new movie |
NSMutableDictionary *assetWriterInputsCorrespondingToOriginalTrackIDs = [NSMutableDictionary dictionary]; |
NSMutableArray *inputsOutputs = [NSMutableArray array]; |
for (AVAssetTrack *track in asset.tracks) |
{ |
NSString *mediaType = track.mediaType; |
// Make the reader |
AVAssetReaderTrackOutput *trackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:track outputSettings:nil]; |
[assetReader addOutput:trackOutput]; |
// Make the writer input, using a source format hint if a format description is available |
AVAssetWriterInput *input; |
CMFormatDescriptionRef formatDescription = CFBridgingRetain([track.formatDescriptions firstObject]); |
if (formatDescription) |
{ |
input = [AVAssetWriterInput assetWriterInputWithMediaType:mediaType outputSettings:nil sourceFormatHint:formatDescription]; |
CFRelease(formatDescription); |
} |
else |
{ |
NSLog(@"skipping track on the assumption that there is no media data to carry over"); |
continue; |
} |
// Carry over language code |
input.languageCode = track.languageCode; |
input.extendedLanguageTag = track.extendedLanguageTag; |
// Copy metadata from the asset track to the asset writer input |
NSMutableArray *trackMetadata = [NSMutableArray array]; |
for (NSString *metadataFormat in track.availableMetadataFormats) |
[trackMetadata addObjectsFromArray:[track metadataForFormat:metadataFormat]]; |
input.metadata = trackMetadata; |
// Add the input, if that's okay to do |
if ([assetWriter canAddInput:input]) |
{ |
[assetWriter addInput:input]; |
// Store the input and output to be used later when actually writing out the new movie |
[inputsOutputs addObject:@{@"input" : input, @"output" : trackOutput}]; |
// Track inputs corresponsing to track IDs for later preservation of track groups |
assetWriterInputsCorrespondingToOriginalTrackIDs[@(track.trackID)] = input; |
} |
else |
{ |
NSLog(@"skipping input because it cannot be added to the asset writer"); |
} |
} |
// Setup the inputs and outputs for new subtitle tracks |
NSMutableArray *newSubtitlesInputs = [NSMutableArray array]; |
NSMutableArray *subtitlesInputsOutputs = [NSMutableArray array]; |
for (NSString *subtitlesPath in subtitlesTextPaths) |
{ |
// Read the contents of the subtitles file |
NSString *text = [NSString stringWithContentsOfURL:[NSURL fileURLWithPath:subtitlesPath] encoding:NSUTF8StringEncoding error:&error]; |
if (!text) |
{ |
NSLog(@"there was a problem reading a subtitles file: %@", error); |
continue; |
} |
// Make the subtitles reader |
SubtitlesTextReader *subtitlesTextReader = [SubtitlesTextReader subtitlesTextReaderWithText:text]; |
// Make the writer input, using a source format hint if a format description is available |
AVAssetWriterInput *subtitlesInput; |
CMFormatDescriptionRef formatDescription = [subtitlesTextReader copyFormatDescription]; |
if (formatDescription) |
{ |
subtitlesInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeSubtitle outputSettings:nil sourceFormatHint:formatDescription]; |
CFRelease(formatDescription); |
} |
else |
{ |
NSLog(@"skipping subtitles reader on the assumption that there is no media data to carry over"); |
continue; |
} |
subtitlesInput.languageCode = subtitlesTextReader.languageCode; |
subtitlesInput.extendedLanguageTag = subtitlesTextReader.extendedLanguageTag; |
subtitlesInput.metadata = subtitlesTextReader.metadata; |
if ([assetWriter canAddInput:subtitlesInput]) |
{ |
[assetWriter addInput:subtitlesInput]; |
// Store the input and output to be used later when actually writing out the new movie |
[subtitlesInputsOutputs addObject:@{@"input" : subtitlesInput, @"output" : subtitlesTextReader}]; |
[newSubtitlesInputs addObject:subtitlesInput]; |
} |
else |
{ |
NSLog(@"skipping subtitles input because it cannot be added to the asset writer"); |
} |
} |
// Preserve track groups from the original asset |
BOOL groupedSubtitles = NO; |
for (AVAssetTrackGroup *trackGroup in asset.trackGroups) |
{ |
// Collect the inputs that correspond to the group's track IDs in an array |
NSMutableArray *inputs = [NSMutableArray array]; |
AVAssetWriterInput *defaultInput; |
for (NSNumber *trackID in trackGroup.trackIDs) |
{ |
AVAssetWriterInput *input = assetWriterInputsCorrespondingToOriginalTrackIDs[trackID]; |
if (input) |
[inputs addObject:input]; |
// Determine which of the inputs is the default according to the enabled state of the corresponding tracks |
if (!defaultInput && [asset trackWithTrackID:(CMPersistentTrackID)[trackID intValue]].enabled) |
defaultInput = input; |
} |
// See if this is a legible (all of the tracks have characteristic AVMediaCharacteristicLegible), and group the new subtitle tracks with it if so |
BOOL isLegibleGroup = NO; |
for (NSNumber *trackID in trackGroup.trackIDs) |
{ |
if ([[asset trackWithTrackID:(CMPersistentTrackID)[trackID intValue]] hasMediaCharacteristic:AVMediaCharacteristicLegible]) |
{ |
isLegibleGroup = YES; |
} |
else if (isLegibleGroup) |
{ |
isLegibleGroup = NO; |
break; |
} |
} |
// If it is a legible group, add the new subtitles to this group |
if (!groupedSubtitles && isLegibleGroup) |
{ |
[inputs addObjectsFromArray:newSubtitlesInputs]; |
groupedSubtitles = YES; |
} |
AVAssetWriterInputGroup *inputGroup = [AVAssetWriterInputGroup assetWriterInputGroupWithInputs:inputs defaultInput:defaultInput]; |
if ([assetWriter canAddInputGroup:inputGroup]) |
{ |
[assetWriter addInputGroup:inputGroup]; |
} |
else |
{ |
NSLog(@"cannot add asset writer group"); |
} |
} |
// If no legible group was found to add the new subtitles to, create a group for them (if there are any) |
if (!groupedSubtitles && (newSubtitlesInputs.count > 0)) |
{ |
AVAssetWriterInputGroup *inputGroup = [AVAssetWriterInputGroup assetWriterInputGroupWithInputs:newSubtitlesInputs defaultInput:nil]; |
if ([assetWriter canAddInputGroup:inputGroup]) |
{ |
[assetWriter addInputGroup:inputGroup]; |
} |
else |
{ |
NSLog(@"cannot add asset writer group"); |
} |
} |
// Preserve track references from original asset |
NSMutableDictionary *trackReferencesCorrespondingToOriginalTrackIDs = [NSMutableDictionary dictionary]; |
for (AVAssetTrack *track in asset.tracks) |
{ |
NSMutableDictionary *trackReferencesForTrack = [NSMutableDictionary dictionary]; |
NSMutableSet *availableTrackAssociatonTypes = [NSMutableSet setWithArray:track.availableTrackAssociationTypes]; |
for (NSString *trackAssociationType in availableTrackAssociatonTypes) |
{ |
NSArray *associatedTracks = [track associatedTracksOfType:trackAssociationType]; |
if (associatedTracks.count > 0) |
{ |
NSMutableArray *associatedTrackIDs = [NSMutableArray arrayWithCapacity:associatedTracks.count]; |
for (AVAssetTrack *associatedTrack in associatedTracks) |
{ |
[associatedTrackIDs addObject:@(associatedTrack.trackID)]; |
} |
trackReferencesForTrack[trackAssociationType] = associatedTrackIDs; |
} |
} |
trackReferencesCorrespondingToOriginalTrackIDs[@(track.trackID)] = trackReferencesForTrack; |
} |
for (NSNumber *referencingTrackIDKey in trackReferencesCorrespondingToOriginalTrackIDs) |
{ |
AVAssetWriterInput *referencingInput = assetWriterInputsCorrespondingToOriginalTrackIDs[referencingTrackIDKey]; |
NSDictionary *trackReferences = trackReferencesCorrespondingToOriginalTrackIDs[referencingTrackIDKey]; |
for (NSString *trackReferenceTypeKey in trackReferences) |
{ |
NSArray *referencedTrackIDs = trackReferences[trackReferenceTypeKey]; |
for (NSNumber *thisReferencedTrackID in referencedTrackIDs) |
{ |
AVAssetWriterInput *referencedInput = assetWriterInputsCorrespondingToOriginalTrackIDs[thisReferencedTrackID]; |
if (referencingInput && referencedInput && [referencingInput canAddTrackAssociationWithTrackOfInput:referencedInput type:trackReferenceTypeKey]) |
[referencingInput addTrackAssociationWithTrackOfInput:referencedInput type:trackReferenceTypeKey]; |
} |
} |
} |
// Write the movie |
if ([assetWriter startWriting]) |
{ |
[assetWriter startSessionAtSourceTime:kCMTimeZero]; |
dispatch_group_t dispatchGroup = dispatch_group_create(); |
[assetReader startReading]; |
// Write samples from AVAssetReaderTrackOutputs |
for (NSDictionary *inputOutput in inputsOutputs) |
{ |
dispatch_group_enter(dispatchGroup); |
dispatch_queue_t requestMediaDataQueue = dispatch_queue_create("request media data", DISPATCH_QUEUE_SERIAL); |
AVAssetWriterInput *input = inputOutput[@"input"]; |
AVAssetReaderTrackOutput *assetReaderTrackOutput = inputOutput[@"output"]; |
[input requestMediaDataWhenReadyOnQueue:requestMediaDataQueue usingBlock:^{ |
while ([input isReadyForMoreMediaData]) |
{ |
CMSampleBufferRef nextSampleBuffer = [assetReaderTrackOutput copyNextSampleBuffer]; |
if (nextSampleBuffer) |
{ |
[input appendSampleBuffer:nextSampleBuffer]; |
CFRelease(nextSampleBuffer); |
} |
else |
{ |
[input markAsFinished]; |
dispatch_group_leave(dispatchGroup); |
if (assetReader.status == AVAssetReaderStatusFailed) |
NSLog(@"the reader failed: %@", assetReader.error); |
break; |
} |
} |
}]; |
} |
// Write samples from SubtitlesTextReaders |
for (NSDictionary *subtitlesInputOutput in subtitlesInputsOutputs) |
{ |
dispatch_group_enter(dispatchGroup); |
dispatch_queue_t requestMediaDataQueue = dispatch_queue_create("request media data", DISPATCH_QUEUE_SERIAL); |
AVAssetWriterInput *input = subtitlesInputOutput[@"input"]; |
SubtitlesTextReader *subtitlesTextReader = subtitlesInputOutput[@"output"]; |
[input requestMediaDataWhenReadyOnQueue:requestMediaDataQueue usingBlock:^{ |
while ([input isReadyForMoreMediaData]) |
{ |
CMSampleBufferRef nextSampleBuffer = [subtitlesTextReader copyNextSampleBuffer]; |
if (nextSampleBuffer) |
{ |
[input appendSampleBuffer:nextSampleBuffer]; |
CFRelease(nextSampleBuffer); |
} |
else |
{ |
[input markAsFinished]; |
dispatch_group_leave(dispatchGroup); |
break; |
} |
} |
}]; |
} |
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER); |
[assetReader cancelReading]; |
dispatch_group_enter(dispatchGroup); |
[assetWriter finishWritingWithCompletionHandler:^(void) { |
if (AVAssetWriterStatusCompleted == assetWriter.status) |
NSLog(@"writing success to %@", assetWriter.outputURL); |
else if (AVAssetWriterStatusFailed == assetWriter.status) |
NSLog (@"writer failed with error: %@", assetWriter.error); |
dispatch_group_leave(dispatchGroup); |
}]; |
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER); |
} |
else |
{ |
NSLog(@"asset writer failed to start writing: %@", assetWriter.error); |
} |
} |
Copyright © 2013 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2013-06-11