1

I’m trying to create a video from images using AVFoundation. There are already multiple threads regarding this approach, but I believe many of them have the same issue as I’m a facing here.

The video plays fine on the iPhone but it doesn’t play on the VLC for example, neither it play correctly on Facebook and Vimeo (sometimes some frames are out of sync). The VLC says that frame rate of the video is 0.58 fps, but it should be more then 24 right?

Does anyone know what is causing this kind of behavior?

Here is the code used to create a video:

self.videoWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:videoOutputPath] fileType:AVFileTypeMPEG4 error:&error];
    // Codec compression settings
    NSDictionary *videoSettings = @{
                                    AVVideoCodecKey : AVVideoCodecH264,
                                    AVVideoWidthKey : @(self.videoSize.width),
                                    AVVideoHeightKey : @(self.videoSize.height),
                                    AVVideoCompressionPropertiesKey : @{
                                            AVVideoAverageBitRateKey : @(20000*1000), // 20 000 kbits/s
                                            AVVideoProfileLevelKey : AVVideoProfileLevelH264High40,
                                            AVVideoMaxKeyFrameIntervalKey : @(1)
                                            }
                                    };

    AVAssetWriterInput* videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];

    AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
                                                     assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput
                                                     sourcePixelBufferAttributes:nil];

    videoWriterInput.expectsMediaDataInRealTime = NO;
    [self.videoWriter addInput:videoWriterInput];
    [self.videoWriter startWriting];
    [self.videoWriter startSessionAtSourceTime:kCMTimeZero];

    [adaptor.assetWriterInput requestMediaDataWhenReadyOnQueue:self.photoToVideoQueue usingBlock:^{
        CMTime time = CMTimeMakeWithSeconds(0, 1000);

        for (Segment* segment in segments) {
            @autoreleasepool {
                UIImage* image = segment.segmentImage;
                CVPixelBufferRef buffer = [self pixelBufferFromImage:image withImageSize:self.videoSize];
                [ImageToVideoManager appendToAdapter:adaptor pixelBuffer:buffer atTime:time];
                CVPixelBufferRelease(buffer);

                CMTime millisecondsDuration = CMTimeMake(segment.durationMS.integerValue, 1000);
                time = CMTimeAdd(time, millisecondsDuration);
            }
        }
        [videoWriterInput markAsFinished];
        [self.videoWriter endSessionAtSourceTime:time];
        [self.videoWriter finishWritingWithCompletionHandler:^{
            NSLog(@"Video writer has finished creating video");
        }];
    }];

- (CVPixelBufferRef)pixelBufferFromImage:(UIImage*)image withImageSize:(CGSize)size{
    CGImageRef cgImage = image.CGImage;
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
                             nil];
    CVPixelBufferRef pxbuffer = NULL;

    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault,
                                          size.width,
                                          size.height,
                                          kCVPixelFormatType_32ARGB,
                                          (__bridge CFDictionaryRef) options,
                                          &pxbuffer);
    if (status != kCVReturnSuccess){
        DebugLog(@"Failed to create pixel buffer");
    }

    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);

    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, 4*size.width, rgbColorSpace, 2);
    CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(cgImage), CGImageGetHeight(cgImage)), cgImage);
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);

    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);

    return pxbuffer;
}

+ (BOOL)appendToAdapter:(AVAssetWriterInputPixelBufferAdaptor*)adaptor
            pixelBuffer:(CVPixelBufferRef)buffer
                 atTime:(CMTime)time{
    while (!adaptor.assetWriterInput.readyForMoreMediaData) {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
    }
    return [adaptor appendPixelBuffer:buffer withPresentationTime:time];
}
aumanets
  • 3,703
  • 8
  • 39
  • 59

1 Answers1

5

Looking at the code, I think the problem is the way you are using the Timestamps...

CMTime consists of a Value and a Timescale. The way I think of this is to treat the timescale portion as essentially the frame rate (this is inaccurate, but a useful mental tool that works well enough for what you're trying to do I think).

The first frame of video at 30FPS would be:

CMTimeMake(1, 30);

Or the 60th frame at 30 frames per second, coincidentally, this is also (60 divide by 30) the 2 second point of your video.

CMTimeMake(60, 30); 

You're specifying 1000 as the timescale, which is way higher than you need. In the loop, you appear to be putting the frame then adding a second and putting another frame. This is what's getting your 0.58 FPS... (although I would have expected 1 FPS, but who knows exactly the intricacies of the codecs).

Instead, what you want to do is loop 30 times (if you want the image to show for 1 second / 30 frames), and put the SAME image on each frame. That should get you to 30 FPS. Of course, you can use a timescale of 24 if you want 24FPS, whatever suits your requirements.

Try re-write this section of your code:

[adaptor.assetWriterInput requestMediaDataWhenReadyOnQueue:self.photoToVideoQueue usingBlock:^{
    CMTime time = CMTimeMakeWithSeconds(0, 1000);

    for (Segment* segment in segments) {
        @autoreleasepool {
            UIImage* image = segment.segmentImage;
            CVPixelBufferRef buffer = [self pixelBufferFromImage:image withImageSize:self.videoSize];
            [ImageToVideoManager appendToAdapter:adaptor pixelBuffer:buffer atTime:time];
            CVPixelBufferRelease(buffer);

            CMTime millisecondsDuration = CMTimeMake(segment.durationMS.integerValue, 1000);
            time = CMTimeAdd(time, millisecondsDuration);
        }
    }
    [videoWriterInput markAsFinished];
    [self.videoWriter endSessionAtSourceTime:time];
    [self.videoWriter finishWritingWithCompletionHandler:^{
        NSLog(@"Video writer has finished creating video");
    }];
}];

As something more like this :

[adaptor.assetWriterInput requestMediaDataWhenReadyOnQueue:self.photoToVideoQueue usingBlock:^{
    // Let's start at the first frame with a timescale of 30 FPS
    CMTime time = CMTimeMake(1, 30);

    for (Segment* segment in segments) {
        @autoreleasepool {
            UIImage* image = segment.segmentImage;
            CVPixelBufferRef buffer = [self pixelBufferFromImage:image withImageSize:self.videoSize];
            for (int i = 1; i <= 30; i++) {
                [ImageToVideoManager appendToAdapter:adaptor pixelBuffer:buffer atTime:time];
                time = CMTimeAdd(time, CMTimeMake(1, 30)); // Add another "frame"
            }
            CVPixelBufferRelease(buffer);

        }
    }
    [videoWriterInput markAsFinished];
    [self.videoWriter endSessionAtSourceTime:time];
    [self.videoWriter finishWritingWithCompletionHandler:^{
        NSLog(@"Video writer has finished creating video");
    }];
}];
Tim Bull
  • 2,375
  • 21
  • 25
  • Hello, I have tried the similar approach as you have specified a few days ago. The problem is that it takes to much time to process, the appendToAdapter method is slow and not always available. Is there any other way to achieve constant frame rate inserting only keyframes? By the way, I'm using time scale of 1000, because I'm working with milliseconds, using this scale it is easy to map each image duration. – aumanets May 21 '14 at 10:40
  • I don't know of another way of doing it. This isn't intended for realtime playback, so it should be OK. You're not inserting KeyFrames - you're inserting frames and allowing the codec to decide which of those are key frames (which you hint it in the options as you've done already). You do have to insert something on every frame. The code I described is on the right path - check this answer http://stackoverflow.com/questions/5640657/avfoundation-assetwriter-generate-movie-with-images-and-audio to see something similar that also work. – Tim Bull May 21 '14 at 15:49
  • I was testing the app on iPhone 5, and with 60 images it takes around 20 seconds to generate the full video (which is used for playback or export). If I use the approach that you have described it would take much more time. Is there any way to insert the keyframe only with the specific duration. I mean, the image is static, why do I need to repeat it 30 times? – aumanets May 21 '14 at 16:17
  • I guess your other solution then is to run it at a much lower FPS (say 5 or 10) - perhaps you can find a frame rate that allows you to insert fewer frames (and therefore faster) but is sufficiently high it will work with the players which sound like they can't cope with the approach you're taking at the moment? – Tim Bull May 21 '14 at 17:26
  • If one should write 30 times an image in order to achieve frame rate of 30 fps per second, why do we have CMTime structure for specifying the time scale and duration? – aumanets May 21 '14 at 19:19
  • I believe you are confusing two separate ideas - Frame Rate and the measurement of Time. You get to a Frame Rate by measuring time, but that doesn't mean that it's the same thing. Inherently video is "something every frame" and a codec is a "way of compressing those somethings efficiently". You can of course, in theory, have a 1 FPS video (I present this frame for 1 second) but you've tried that and it doesn't play properly in some players. Good luck with trying to solve your problem, I believe the answer is in setting a more rational timescale, perhaps someone else can help you more. – Tim Bull May 21 '14 at 20:59
  • 1
    Probably there is no other way to solve the problem I've described here. You explanation was clear and useful. Thanks. – aumanets May 26 '14 at 09:36