Làm cách nào để xuất mảng UIImage dưới dạng phim?


186

Tôi có một vấn đề nghiêm trọng: tôi có một NSArrayvài UIImageđối tượng. Những gì tôi muốn làm bây giờ là tạo phim từ những thứ đó UIImages. Nhưng tôi không biết làm thế nào để làm như vậy.

Tôi hy vọng ai đó có thể giúp tôi hoặc gửi cho tôi một đoạn mã làm điều gì đó như tôi muốn.

Chỉnh sửa: Để tham khảo trong tương lai - Sau khi áp dụng giải pháp, nếu video có vẻ bị méo, hãy đảm bảo độ rộng của hình ảnh / khu vực bạn đang chụp là bội số của 16. Được tìm thấy sau nhiều giờ đấu tranh tại đây:
Tại sao phim của tôi từ UIImages lại bị méo mó?

Đây là giải pháp hoàn chỉnh (chỉ cần đảm bảo chiều rộng là bội số của 16)
http://codethink.no-ip.org/wordpress/archives/673


@zoul: Thẻ nên bao gồm những gì câu hỏi là về, không phải là giải pháp có thể.
Georg Fritzsche

4
Tại sao không? Đã có một bài đăng cho cả AVFoundationvà FFmpeg. Nếu bạn đang tìm kiếm một số thông tin liên quan đến AVFoundation, bạn có muốn xem chủ đề này không? (Hay đó là sự đồng thuận từ Meta?)
zoul

@zoul: Các thẻ thu hẹp câu hỏi ( "Thẻ là từ khóa hoặc nhãn phân loại câu hỏi của bạn" ), với việc thêm hai câu hỏi mà bạn sẽ thay đổi ngữ cảnh. Tôi nghĩ điều này là hiển nhiên nhưng nếu tôi vấp ngã về một cái gì đó trên meta tôi sẽ cho bạn biết. Hoặc bắt đầu một cuộc thảo luận ở đó.
Georg Fritzsche

3
Có thể sẽ hữu ích cho ai đó - mã của tôi trên github github.com/sakrist/One-minute
SAKrisT

Không có Dana, chỉ có Zoul. [xin lỗi vì lạc đề, nhưng tôi không thể cưỡng lại]
Rob VS

Câu trả lời:


217

Hãy xem AVAssetWriter và phần còn lại của khung AVFoundation . Người viết có một kiểu đầu vào AVAssetWriterInput , lần lượt có một phương thức gọi là appendSampleBuffer: cho phép bạn thêm các khung riêng lẻ vào luồng video. Về cơ bản bạn sẽ phải:

1) Dây người viết:

NSError *error = nil;
AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:
    [NSURL fileURLWithPath:somePath] fileType:AVFileTypeQuickTimeMovie
    error:&error];
NSParameterAssert(videoWriter);

NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
    AVVideoCodecH264, AVVideoCodecKey,
    [NSNumber numberWithInt:640], AVVideoWidthKey,
    [NSNumber numberWithInt:480], AVVideoHeightKey,
    nil];
AVAssetWriterInput* writerInput = [[AVAssetWriterInput
    assetWriterInputWithMediaType:AVMediaTypeVideo
    outputSettings:videoSettings] retain]; //retain should be removed if ARC

NSParameterAssert(writerInput);
NSParameterAssert([videoWriter canAddInput:writerInput]);
[videoWriter addInput:writerInput];

2) Bắt đầu một phiên:

[videoWriter startWriting];
[videoWriter startSessionAtSourceTime:…] //use kCMTimeZero if unsure

3) Viết một số mẫu:

// Or you can use AVAssetWriterInputPixelBufferAdaptor.
// That lets you feed the writer input data from a CVPixelBuffer
// that’s quite easy to create from a CGImage.
[writerInput appendSampleBuffer:sampleBuffer];

4) Kết thúc phiên:

[writerInput markAsFinished];
[videoWriter endSessionAtSourceTime:…]; //optional can call finishWriting without specifying endTime
[videoWriter finishWriting]; //deprecated in ios6
/*
[videoWriter finishWritingWithCompletionHandler:...]; //ios 6.0+
*/

Bạn vẫn sẽ phải điền vào rất nhiều khoảng trống, nhưng tôi nghĩ rằng phần thực sự khó khăn duy nhất còn lại là lấy bộ đệm pixel từ CGImage:

- (CVPixelBufferRef) newPixelBufferFromCGImage: (CGImageRef) image
{
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
        [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
        [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
        nil];
    CVPixelBufferRef pxbuffer = NULL;
    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, frameSize.width,
        frameSize.height, kCVPixelFormatType_32ARGB, (CFDictionaryRef) options, 
        &pxbuffer);
    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);

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

    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(pxdata, frameSize.width,
        frameSize.height, 8, 4*frameSize.width, rgbColorSpace, 
        kCGImageAlphaNoneSkipFirst);
    NSParameterAssert(context);
    CGContextConcatCTM(context, frameTransform);
    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), 
        CGImageGetHeight(image)), image);
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);

    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);

    return pxbuffer;
}

frameSizelà một CGSizemô tả kích thước khung mục tiêu của bạn và frameTransformlà một CGAffineTransformcho phép bạn chuyển đổi hình ảnh khi bạn vẽ chúng vào khung.


2
Mặc dù điều này không hiệu quả, nhưng việc vẽ thành một thứ CGImageduy nhất để vẽ nó thành một CGBitmapContexthậu thuẫn CVPixelBufferlà lãng phí. Tương tự như vậy, thay vì tạo một CVPixelBuffermỗi lần, AVAssetWriterInputPixelBufferAdaptor's pixelBufferPoolnên được sử dụng để đệm rác.
rpetrich

8
Vậy thì bạn nên làm gì sau đó, khi bạn có dữ liệu nguồn như các tệp hình ảnh thông thường?
zoul

3
Tôi với zoul ... chúng ta nên làm gì nếu nguồn dữ liệu của chúng ta có nhiều hình ảnh? tôi chạy vào các cảnh báo bộ nhớ trong khoảng 30 giây hoặc ít hơn vài giây để mã hóa hình ảnh thành .mov và tôi không thể tìm ra nơi bộ nhớ đang hình thành
james

9
@rpetrich @zoul Tôi có thể hỏi tại sao sử dụng CVPixelBuffer thay vì CMSampleBufferRef không? Không phải CMSampleBufferRef là loại tham số appendSampleBuffer:? BTW, tôi đang sử dụng AVFoundation trong OS X.
Andrew Chang

12
Điều này sẽ giúp ai đó, một ngày stackoverflow.com/questions/9691646/ từ
DogCoffee

41

Dưới đây là mã làm việc mới nhất trên iOS8 trong Objective-C.

Chúng tôi đã phải thực hiện một số điều chỉnh cho câu trả lời của @ Zoul ở trên để làm cho nó hoạt động trên phiên bản Xcode và iOS8 mới nhất. Đây là mã làm việc hoàn chỉnh của chúng tôi, lấy một mảng UIImages, biến chúng thành tệp .mov, lưu nó vào thư mục tạm thời, sau đó di chuyển nó vào cuộn camera. Chúng tôi tập hợp mã từ nhiều bài viết khác nhau để làm việc này. Chúng tôi đã nhấn mạnh các bẫy mà chúng tôi phải giải quyết để làm cho mã hoạt động trong các bình luận của chúng tôi.

(1) Tạo một bộ sưu tập UIImages

[self saveMovieToLibrary]


- (IBAction)saveMovieToLibrary
{
    // You just need the height and width of the video here
    // For us, our input and output video was 640 height x 480 width
    // which is what we get from the iOS front camera
    ATHSingleton *singleton = [ATHSingleton singletons];
    int height = singleton.screenHeight;
    int width = singleton.screenWidth;

    // You can save a .mov or a .mp4 file        
    //NSString *fileNameOut = @"temp.mp4";
    NSString *fileNameOut = @"temp.mov";

    // We chose to save in the tmp/ directory on the device initially
    NSString *directoryOut = @"tmp/";
    NSString *outFile = [NSString stringWithFormat:@"%@%@",directoryOut,fileNameOut];
    NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:outFile]];
    NSURL *videoTempURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), fileNameOut]];

    // WARNING: AVAssetWriter does not overwrite files for us, so remove the destination file if it already exists
    NSFileManager *fileManager = [NSFileManager defaultManager];
    [fileManager removeItemAtPath:[videoTempURL path]  error:NULL];


    // Create your own array of UIImages        
    NSMutableArray *images = [NSMutableArray array];
    for (int i=0; i<singleton.numberOfScreenshots; i++)
    {
        // This was our routine that returned a UIImage. Just use your own.
        UIImage *image =[self uiimageFromCopyOfPixelBuffersUsingIndex:i];
        // We used a routine to write text onto every image 
        // so we could validate the images were actually being written when testing. This was it below. 
        image = [self writeToImage:image Text:[NSString stringWithFormat:@"%i",i ]];
        [images addObject:image];     
    }

// If you just want to manually add a few images - here is code you can uncomment
// NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"Documents/movie.mp4"]];
//    NSArray *images = [[NSArray alloc] initWithObjects:
//                      [UIImage imageNamed:@"add_ar.png"],
//                      [UIImage imageNamed:@"add_ja.png"],
//                      [UIImage imageNamed:@"add_ru.png"],
//                      [UIImage imageNamed:@"add_ru.png"],
//                      [UIImage imageNamed:@"add_ar.png"],
//                      [UIImage imageNamed:@"add_ja.png"],
//                      [UIImage imageNamed:@"add_ru.png"],
//                      [UIImage imageNamed:@"add_ar.png"],
//                      [UIImage imageNamed:@"add_en.png"], nil];



    [self writeImageAsMovie:images toPath:path size:CGSizeMake(height, width)];
}

Đây là phương pháp chính tạo AssetWriter của bạn và thêm hình ảnh vào đó để viết.

(2) Kết nối AVAssetWriter

-(void)writeImageAsMovie:(NSArray *)array toPath:(NSString*)path size:(CGSize)size
{

    NSError *error = nil;

    // FIRST, start up an AVAssetWriter instance to write your video
    // Give it a destination path (for us: tmp/temp.mov)
    AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:path]
                                                           fileType:AVFileTypeQuickTimeMovie
                                                              error:&error];


    NSParameterAssert(videoWriter);

    NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                   AVVideoCodecH264, AVVideoCodecKey,
                                   [NSNumber numberWithInt:size.width], AVVideoWidthKey,
                                   [NSNumber numberWithInt:size.height], AVVideoHeightKey,
                                   nil];

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

    AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:writerInput
                                                                                                                     sourcePixelBufferAttributes:nil];
    NSParameterAssert(writerInput);
    NSParameterAssert([videoWriter canAddInput:writerInput]);
    [videoWriter addInput:writerInput];

(3) Bắt đầu một phiên viết (LƯU Ý: phương pháp đang tiếp tục từ phía trên)

    //Start a SESSION of writing. 
    // After you start a session, you will keep adding image frames 
    // until you are complete - then you will tell it you are done.
    [videoWriter startWriting];
    // This starts your video at time = 0
    [videoWriter startSessionAtSourceTime:kCMTimeZero];

    CVPixelBufferRef buffer = NULL;

    // This was just our utility class to get screen sizes etc.    
    ATHSingleton *singleton = [ATHSingleton singletons];

    int i = 0;
    while (1)
    {
        // Check if the writer is ready for more data, if not, just wait
        if(writerInput.readyForMoreMediaData){

            CMTime frameTime = CMTimeMake(150, 600);
            // CMTime = Value and Timescale.
            // Timescale = the number of tics per second you want
            // Value is the number of tics
            // For us - each frame we add will be 1/4th of a second
            // Apple recommend 600 tics per second for video because it is a 
            // multiple of the standard video rates 24, 30, 60 fps etc.
            CMTime lastTime=CMTimeMake(i*150, 600);
            CMTime presentTime=CMTimeAdd(lastTime, frameTime);

            if (i == 0) {presentTime = CMTimeMake(0, 600);} 
            // This ensures the first frame starts at 0.


            if (i >= [array count])
            {
                buffer = NULL;
            }
            else
            {
                // This command grabs the next UIImage and converts it to a CGImage
                buffer = [self pixelBufferFromCGImage:[[array objectAtIndex:i] CGImage]];
            }


            if (buffer)
            {
                // Give the CGImage to the AVAssetWriter to add to your video
                [adaptor appendPixelBuffer:buffer withPresentationTime:presentTime];
                i++;
            }
            else
            {

(4) Kết thúc phiên (Lưu ý: Phương pháp tiếp tục từ phía trên)

                //Finish the session:
                // This is important to be done exactly in this order
                [writerInput markAsFinished];
                // WARNING: finishWriting in the solution above is deprecated. 
                // You now need to give a completion handler.
                [videoWriter finishWritingWithCompletionHandler:^{
                    NSLog(@"Finished writing...checking completion status...");
                    if (videoWriter.status != AVAssetWriterStatusFailed && videoWriter.status == AVAssetWriterStatusCompleted)
                    {
                        NSLog(@"Video writing succeeded.");

                        // Move video to camera roll
                        // NOTE: You cannot write directly to the camera roll. 
                        // You must first write to an iOS directory then move it!
                        NSURL *videoTempURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@", path]];
                        [self saveToCameraRoll:videoTempURL];

                    } else
                    {
                        NSLog(@"Video writing failed: %@", videoWriter.error);
                    }

                }]; // end videoWriter finishWriting Block

                CVPixelBufferPoolRelease(adaptor.pixelBufferPool);

                NSLog (@"Done");
                break;
            }
        }
    }    
}

(5) Chuyển đổi UII của bạn thành CVPixelBufferRef
Phương pháp này sẽ cung cấp cho bạn một tham chiếu bộ đệm pixel CV cần thiết cho AssetWriter. Điều này được lấy từ CGImageRef mà bạn nhận được từ UIImage của mình (ở trên).

- (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image
{
    // This again was just our utility class for the height & width of the
    // incoming video (640 height x 480 width)
    ATHSingleton *singleton = [ATHSingleton singletons];
    int height = singleton.screenHeight;
    int width = singleton.screenWidth;

    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
                             nil];
    CVPixelBufferRef pxbuffer = NULL;

    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, width,
                                          height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options,
                                          &pxbuffer);

    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);

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

    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();

    CGContextRef context = CGBitmapContextCreate(pxdata, width,
                                                 height, 8, 4*width, rgbColorSpace,
                                                 kCGImageAlphaNoneSkipFirst);
    NSParameterAssert(context);
    CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
                                           CGImageGetHeight(image)), image);
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);

    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);

    return pxbuffer;
}

(6) Di chuyển video của bạn sang cuộn camera vì AVAssetWriter không thể ghi trực tiếp vào cuộn camera, điều này sẽ chuyển video từ "tmp / temp.mov" (hoặc bất kỳ tên tệp nào bạn đặt tên ở trên) sang cuộn camera.

- (void) saveToCameraRoll:(NSURL *)srcURL
{
    NSLog(@"srcURL: %@", srcURL);

    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    ALAssetsLibraryWriteVideoCompletionBlock videoWriteCompletionBlock =
    ^(NSURL *newURL, NSError *error) {
        if (error) {
            NSLog( @"Error writing image with metadata to Photo Library: %@", error );
        } else {
            NSLog( @"Wrote image with metadata to Photo Library %@", newURL.absoluteString);
        }
    };

    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:srcURL])
    {
        [library writeVideoAtPathToSavedPhotosAlbum:srcURL
                                    completionBlock:videoWriteCompletionBlock];
    }
}

Câu trả lời của Zoul ở trên đưa ra một phác thảo tốt đẹp về những gì bạn sẽ làm. Chúng tôi đã nhận xét rộng rãi mã này để bạn có thể thấy nó đã được thực hiện bằng cách sử dụng mã làm việc như thế nào.


3
Bất cứ ai cũng có thể có một giải pháp trong Swift 2.1?
Kyle KIM

2
Có ai có một giải pháp trong Swift?
Sam

2
@KyleKIM bạn đã tìm thấy một giải pháp trong Swift?
Sam

2
@Meseery Để thêm văn bản, chỉ cần viết văn bản lên hình ảnh mà bạn đang thêm vào video nhưng trước khi bạn thêm chúng. Có rất nhiều ví dụ về cách viết văn bản thành hình ảnh trên SO. Đối với âm thanh, tôi hy vọng bạn có thể thêm video đó vào video sau khi hoàn thành khía cạnh video.
Praxlistes

3
Bất cứ ai cũng có một giải pháp cho vấn đề bộ nhớ? Khoảng 3 hoặc 4 trăm hình ảnh, ứng dụng của tôi bị giết. Tôi đang chuyển một mảng các đường dẫn tệp hình ảnh và tải từng hình ảnh theo yêu cầu, nhưng có vẻ như bộ nhớ vẫn còn lưu lại
beebcon

31

LƯU Ý: Đây là giải pháp Swift 2.1 (iOS8 +, XCode 7.2) .

Tuần trước tôi đã bắt đầu viết mã iOS để tạo video từ hình ảnh. Tôi đã có một chút kinh nghiệm AVFoundation, nhưng thậm chí chưa bao giờ nghe nói về CVPixelBuffer. Tôi đã xem qua các câu trả lời trên trang này và cũng ở đây . Phải mất vài ngày để mổ xẻ mọi thứ và kết hợp tất cả lại với nhau trong Swift theo cách có ý nghĩa với bộ não của tôi. Dưới đây là những gì tôi đã đưa ra.

LƯU Ý: Nếu bạn sao chép / dán tất cả mã bên dưới vào một tệp Swift, nó sẽ biên dịch. Bạn sẽ chỉ cần điều chỉnh loadImages()và các RenderSettingsgiá trị.

Phần 1: Thiết lập mọi thứ

Ở đây tôi nhóm tất cả các cài đặt liên quan đến xuất thành một RenderSettingscấu trúc duy nhất .

import AVFoundation
import UIKit
import Photos

struct RenderSettings {

    var width: CGFloat = 1280
    var height: CGFloat = 720
    var fps: Int32 = 2   // 2 frames per second
    var avCodecKey = AVVideoCodecH264
    var videoFilename = "render"
    var videoFilenameExt = "mp4"

    var size: CGSize {
        return CGSize(width: width, height: height)
    }

    var outputURL: NSURL {
        // Use the CachesDirectory so the rendered video file sticks around as long as we need it to.
        // Using the CachesDirectory ensures the file won't be included in a backup of the app.
        let fileManager = NSFileManager.defaultManager()
        if let tmpDirURL = try? fileManager.URLForDirectory(.CachesDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true) {
            return tmpDirURL.URLByAppendingPathComponent(videoFilename).URLByAppendingPathExtension(videoFilenameExt)
        }
        fatalError("URLForDirectory() failed")
    }
}

Phần 2: ImageAnimator

Các ImageAnimator lớp biết về hình ảnh của bạn và sử dụng các VideoWriterlớp để thực hiện các rendering. Ý tưởng là giữ cho mã nội dung video tách biệt với mã AVFoundation cấp thấp. Tôi cũng đã thêm vào saveToLibrary()đây như là một chức năng lớp được gọi ở cuối chuỗi để lưu video vào Thư viện ảnh.

class ImageAnimator {

    // Apple suggests a timescale of 600 because it's a multiple of standard video rates 24, 25, 30, 60 fps etc.
    static let kTimescale: Int32 = 600

    let settings: RenderSettings
    let videoWriter: VideoWriter
    var images: [UIImage]!

    var frameNum = 0

    class func saveToLibrary(videoURL: NSURL) {
        PHPhotoLibrary.requestAuthorization { status in
            guard status == .Authorized else { return }

            PHPhotoLibrary.sharedPhotoLibrary().performChanges({
                PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(videoURL)
                }) { success, error in
                    if !success {
                        print("Could not save video to photo library:", error)
                    }
            }
        }
    }

    class func removeFileAtURL(fileURL: NSURL) {
        do {
            try NSFileManager.defaultManager().removeItemAtPath(fileURL.path!)
        }
        catch _ as NSError {
            // Assume file doesn't exist.
        }
    }

    init(renderSettings: RenderSettings) {
        settings = renderSettings
        videoWriter = VideoWriter(renderSettings: settings)
        images = loadImages()
    }

    func render(completion: ()->Void) {

        // The VideoWriter will fail if a file exists at the URL, so clear it out first.
        ImageAnimator.removeFileAtURL(settings.outputURL)

        videoWriter.start()
        videoWriter.render(appendPixelBuffers) {
            ImageAnimator.saveToLibrary(self.settings.outputURL)
            completion()
        }

    }

    // Replace this logic with your own.
    func loadImages() -> [UIImage] {
        var images = [UIImage]()
        for index in 1...10 {
            let filename = "\(index).jpg"
            images.append(UIImage(named: filename)!)
        }
        return images
    }

    // This is the callback function for VideoWriter.render()
    func appendPixelBuffers(writer: VideoWriter) -> Bool {

        let frameDuration = CMTimeMake(Int64(ImageAnimator.kTimescale / settings.fps), ImageAnimator.kTimescale)

        while !images.isEmpty {

            if writer.isReadyForData == false {
                // Inform writer we have more buffers to write.
                return false
            }

            let image = images.removeFirst()
            let presentationTime = CMTimeMultiply(frameDuration, Int32(frameNum))
            let success = videoWriter.addImage(image, withPresentationTime: presentationTime)
            if success == false {
                fatalError("addImage() failed")
            }

            frameNum++
        }

        // Inform writer all buffers have been written.
        return true
    }

}

Phần 3: VideoWriter

Các VideoWriterlớp học thực hiện tất cả AVFoundation nâng nặng. Nó chủ yếu là một bọc xung quanh AVAssetWriterAVAssetWriterInput . Nó cũng chứa mã ưa thích được viết bởi không phải tôi biết cách dịch một hình ảnh thành a CVPixelBuffer.

class VideoWriter {

    let renderSettings: RenderSettings

    var videoWriter: AVAssetWriter!
    var videoWriterInput: AVAssetWriterInput!
    var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor!

    var isReadyForData: Bool {
        return videoWriterInput?.readyForMoreMediaData ?? false
    }

    class func pixelBufferFromImage(image: UIImage, pixelBufferPool: CVPixelBufferPool, size: CGSize) -> CVPixelBuffer {

        var pixelBufferOut: CVPixelBuffer?

        let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &pixelBufferOut)
        if status != kCVReturnSuccess {
            fatalError("CVPixelBufferPoolCreatePixelBuffer() failed")
        }

        let pixelBuffer = pixelBufferOut!

        CVPixelBufferLockBaseAddress(pixelBuffer, 0)

        let data = CVPixelBufferGetBaseAddress(pixelBuffer)
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGBitmapContextCreate(data, Int(size.width), Int(size.height),
            8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, CGImageAlphaInfo.PremultipliedFirst.rawValue)

        CGContextClearRect(context, CGRectMake(0, 0, size.width, size.height))

        let horizontalRatio = size.width / image.size.width
        let verticalRatio = size.height / image.size.height
        //aspectRatio = max(horizontalRatio, verticalRatio) // ScaleAspectFill
        let aspectRatio = min(horizontalRatio, verticalRatio) // ScaleAspectFit

        let newSize = CGSize(width: image.size.width * aspectRatio, height: image.size.height * aspectRatio)

        let x = newSize.width < size.width ? (size.width - newSize.width) / 2 : 0
        let y = newSize.height < size.height ? (size.height - newSize.height) / 2 : 0

        CGContextDrawImage(context, CGRectMake(x, y, newSize.width, newSize.height), image.CGImage)
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0)

        return pixelBuffer
    }

    init(renderSettings: RenderSettings) {
        self.renderSettings = renderSettings
    }

    func start() {

        let avOutputSettings: [String: AnyObject] = [
            AVVideoCodecKey: renderSettings.avCodecKey,
            AVVideoWidthKey: NSNumber(float: Float(renderSettings.width)),
            AVVideoHeightKey: NSNumber(float: Float(renderSettings.height))
        ]

        func createPixelBufferAdaptor() {
            let sourcePixelBufferAttributesDictionary = [
                kCVPixelBufferPixelFormatTypeKey as String: NSNumber(unsignedInt: kCVPixelFormatType_32ARGB),
                kCVPixelBufferWidthKey as String: NSNumber(float: Float(renderSettings.width)),
                kCVPixelBufferHeightKey as String: NSNumber(float: Float(renderSettings.height))
            ]
            pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput,
                sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary)
        }

        func createAssetWriter(outputURL: NSURL) -> AVAssetWriter {
            guard let assetWriter = try? AVAssetWriter(URL: outputURL, fileType: AVFileTypeMPEG4) else {
                fatalError("AVAssetWriter() failed")
            }

            guard assetWriter.canApplyOutputSettings(avOutputSettings, forMediaType: AVMediaTypeVideo) else {
                fatalError("canApplyOutputSettings() failed")
            }

            return assetWriter
        }

        videoWriter = createAssetWriter(renderSettings.outputURL)
        videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: avOutputSettings)

        if videoWriter.canAddInput(videoWriterInput) {
            videoWriter.addInput(videoWriterInput)
        }
        else {
            fatalError("canAddInput() returned false")
        }

        // The pixel buffer adaptor must be created before we start writing.
        createPixelBufferAdaptor()

        if videoWriter.startWriting() == false {
            fatalError("startWriting() failed")
        }

        videoWriter.startSessionAtSourceTime(kCMTimeZero)

        precondition(pixelBufferAdaptor.pixelBufferPool != nil, "nil pixelBufferPool")
    }

    func render(appendPixelBuffers: (VideoWriter)->Bool, completion: ()->Void) {

        precondition(videoWriter != nil, "Call start() to initialze the writer")

        let queue = dispatch_queue_create("mediaInputQueue", nil)
        videoWriterInput.requestMediaDataWhenReadyOnQueue(queue) {
            let isFinished = appendPixelBuffers(self)
            if isFinished {
                self.videoWriterInput.markAsFinished()
                self.videoWriter.finishWritingWithCompletionHandler() {
                    dispatch_async(dispatch_get_main_queue()) {
                        completion()
                    }
                }
            }
            else {
                // Fall through. The closure will be called again when the writer is ready.
            }
        }
    }

    func addImage(image: UIImage, withPresentationTime presentationTime: CMTime) -> Bool {

        precondition(pixelBufferAdaptor != nil, "Call start() to initialze the writer")

        let pixelBuffer = VideoWriter.pixelBufferFromImage(image, pixelBufferPool: pixelBufferAdaptor.pixelBufferPool!, size: renderSettings.size)
        return pixelBufferAdaptor.appendPixelBuffer(pixelBuffer, withPresentationTime: presentationTime)
    }

}

Phần 4: Làm cho nó xảy ra

Khi mọi thứ đã ổn định, đây là 3 dòng ma thuật của bạn:

let settings = RenderSettings()
let imageAnimator = ImageAnimator(renderSettings: settings)
imageAnimator.render() {
    print("yes")
}

Ôi trời, ước gì bạn đã đăng sớm hơn một ngày. :) Vừa hoàn thành việc chuyển một phiên bản Swift, thêm vào chủ đề này. Kết hợp lưu của bạn vào chức năng thư viện, hy vọng bạn không phiền.
Crashalot

@Crashalot - Tôi nghe thấy bạn! Tôi có cảm giác nó đang được làm việc. Tuy nhiên, là một bài tập tốt để hiểu tất cả và phá vỡ các bit khác nhau.
scootermg

Crashalot và @Scott Raposa. Đây là một nhận xét liên quan đến câu hỏi này nhưng trong một trường hợp thử nghiệm đặc biệt. Mã của bạn hoàn toàn phi thường và đáng kinh ngạc, nhưng dường như nó không hoạt động đối với hình ảnh SINGLE, trong đó hình ảnh.count == 1. Tôi đã thay đổi mã để thử và giải quyết vấn đề này, nhưng dường như vô cùng khó khăn. Bất kỳ trợ giúp từ các bạn sẽ hoàn toàn tuyệt vời. Tôi cũng đã hỏi một câu hỏi ở đây tại stackoverflow.com/questions/38035809/ triệt . Tôi đã hy vọng trường hợp đặc biệt của hình ảnh.count == 1 có thể được giải quyết! Cảm ơn!
ấn

@ Impress7vx Hiện tại tôi không có mã đang chạy nên tôi không thể kiểm tra cụ thể cho trường hợp đó. Tôi không chắc tại sao nó không hoạt động. Khi bạn nói nó không hoạt động, bạn đang thấy gì? Có thể chơi với các thiết lập khung hình / giây. Đưa nó về 1. Hoặc nếu bạn có nó hoạt động với 2 hình ảnh, sau đó chỉ cần nhân đôi hình ảnh.
scootermg

Nói rằng tôi có 1 hình ảnh, ngay bây giờ tôi đang sao chép nó và nó hoạt động. Nhưng vì lý do gì nó không hoạt động với một hình ảnh duy nhất. Tôi hình dung tôi sẽ chỉ thông báo cho bạn! Sử dụng images = [image, image]tác phẩm nhưng một hình ảnh duy nhất thì không. Giả sử tôi thay đổi thời gian thành 10 giây, sau đó tôi thấy một màn hình đen cho đến khung hình cuối cùng. Khá lạ.
ấn

21

Tôi đã lấy ý tưởng chính của Zoul và kết hợp phương pháp AVAssetWriterInputPixelBufferAdaptor và thực hiện sự khởi đầu của một số khung nhỏ từ nó.

Hãy kiểm tra và cải thiện nó! CEMovieMaker


2
@CameronE Tốt một nhưng tôi có một vấn đề, Nếu video của tôi là 1080 * 1920 thì sao? iPhone 5s, độ phân giải camera phía sau 6,6plus, Video lộn xộn trong tình huống này xin vui lòng giúp tôi.
Dipen Chudasama

1
Xin chào, bạn có thể cho tôi biết làm cách nào tôi có thể đặt tốc độ thấp hơn theo cách hình ảnh xuất hiện trong video không?
Hima

Làm cách nào để thêm một số độ trễ trong video như thay đổi thời gian cho khung hình nào sẽ xuất hiện?
ViruMax

13

Đây là phiên bản Swift 2.x được thử nghiệm trên iOS 8. Nó kết hợp các câu trả lời từ @Scott Raposa và @Praxiteles cùng với mã từ @acj đã đóng góp cho một câu hỏi khác. Mã từ @acj có tại đây: https://gist.github.com/acj/6ae90aa1ebb8cad6b47b . @TimBull cũng cung cấp mã là tốt.

Giống như @Scott Raposa, tôi thậm chí chưa bao giờ nghe nói về CVPixelBufferPoolCreatePixelBuffer và một số chức năng khác, chứ chưa nói đến việc hiểu cách sử dụng chúng.

Những gì bạn thấy dưới đây đã được ghép lại với nhau chủ yếu bằng bản dùng thử và lỗi và từ việc đọc tài liệu của Apple. Vui lòng sử dụng một cách thận trọng và cung cấp các đề xuất nếu có sai lầm.

Sử dụng:

import UIKit
import AVFoundation
import Photos

writeImagesAsMovie(yourImages, videoPath: yourPath, videoSize: yourSize, videoFPS: 30)

Mã số:

func writeImagesAsMovie(allImages: [UIImage], videoPath: String, videoSize: CGSize, videoFPS: Int32) {
    // Create AVAssetWriter to write video
    guard let assetWriter = createAssetWriter(videoPath, size: videoSize) else {
        print("Error converting images to video: AVAssetWriter not created")
        return
    }

    // If here, AVAssetWriter exists so create AVAssetWriterInputPixelBufferAdaptor
    let writerInput = assetWriter.inputs.filter{ $0.mediaType == AVMediaTypeVideo }.first!
    let sourceBufferAttributes : [String : AnyObject] = [
        kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32ARGB),
        kCVPixelBufferWidthKey as String : videoSize.width,
        kCVPixelBufferHeightKey as String : videoSize.height,
        ]
    let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: sourceBufferAttributes)

    // Start writing session
    assetWriter.startWriting()
    assetWriter.startSessionAtSourceTime(kCMTimeZero)
    if (pixelBufferAdaptor.pixelBufferPool == nil) {
        print("Error converting images to video: pixelBufferPool nil after starting session")
        return
    }

    // -- Create queue for <requestMediaDataWhenReadyOnQueue>
    let mediaQueue = dispatch_queue_create("mediaInputQueue", nil)

    // -- Set video parameters
    let frameDuration = CMTimeMake(1, videoFPS)
    var frameCount = 0

    // -- Add images to video
    let numImages = allImages.count
    writerInput.requestMediaDataWhenReadyOnQueue(mediaQueue, usingBlock: { () -> Void in
        // Append unadded images to video but only while input ready
        while (writerInput.readyForMoreMediaData && frameCount < numImages) {
            let lastFrameTime = CMTimeMake(Int64(frameCount), videoFPS)
            let presentationTime = frameCount == 0 ? lastFrameTime : CMTimeAdd(lastFrameTime, frameDuration)

            if !self.appendPixelBufferForImageAtURL(allImages[frameCount], pixelBufferAdaptor: pixelBufferAdaptor, presentationTime: presentationTime) {
                print("Error converting images to video: AVAssetWriterInputPixelBufferAdapter failed to append pixel buffer")
                return
            }

            frameCount += 1
        }

        // No more images to add? End video.
        if (frameCount >= numImages) {
            writerInput.markAsFinished()
            assetWriter.finishWritingWithCompletionHandler {
                if (assetWriter.error != nil) {
                    print("Error converting images to video: \(assetWriter.error)")
                } else {
                    self.saveVideoToLibrary(NSURL(fileURLWithPath: videoPath))
                    print("Converted images to movie @ \(videoPath)")
                }
            }
        }
    })
}


func createAssetWriter(path: String, size: CGSize) -> AVAssetWriter? {
    // Convert <path> to NSURL object
    let pathURL = NSURL(fileURLWithPath: path)

    // Return new asset writer or nil
    do {
        // Create asset writer
        let newWriter = try AVAssetWriter(URL: pathURL, fileType: AVFileTypeMPEG4)

        // Define settings for video input
        let videoSettings: [String : AnyObject] = [
            AVVideoCodecKey  : AVVideoCodecH264,
            AVVideoWidthKey  : size.width,
            AVVideoHeightKey : size.height,
            ]

        // Add video input to writer
        let assetWriterVideoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings)
        newWriter.addInput(assetWriterVideoInput)

        // Return writer
        print("Created asset writer for \(size.width)x\(size.height) video")
        return newWriter
    } catch {
        print("Error creating asset writer: \(error)")
        return nil
    }
}


func appendPixelBufferForImageAtURL(image: UIImage, pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool {
    var appendSucceeded = false

    autoreleasepool {
        if  let pixelBufferPool = pixelBufferAdaptor.pixelBufferPool {
            let pixelBufferPointer = UnsafeMutablePointer<CVPixelBuffer?>.alloc(1)
            let status: CVReturn = CVPixelBufferPoolCreatePixelBuffer(
                kCFAllocatorDefault,
                pixelBufferPool,
                pixelBufferPointer
            )

            if let pixelBuffer = pixelBufferPointer.memory where status == 0 {
                fillPixelBufferFromImage(image, pixelBuffer: pixelBuffer)
                appendSucceeded = pixelBufferAdaptor.appendPixelBuffer(pixelBuffer, withPresentationTime: presentationTime)
                pixelBufferPointer.destroy()
            } else {
                NSLog("Error: Failed to allocate pixel buffer from pool")
            }

            pixelBufferPointer.dealloc(1)
        }
    }

    return appendSucceeded
}


func fillPixelBufferFromImage(image: UIImage, pixelBuffer: CVPixelBufferRef) {
    CVPixelBufferLockBaseAddress(pixelBuffer, 0)

    let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer)
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()

    // Create CGBitmapContext
    let context = CGBitmapContextCreate(
        pixelData,
        Int(image.size.width),
        Int(image.size.height),
        8,
        CVPixelBufferGetBytesPerRow(pixelBuffer),
        rgbColorSpace,
        CGImageAlphaInfo.PremultipliedFirst.rawValue
    )

    // Draw image into context
    CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage)

    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0)
}


func saveVideoToLibrary(videoURL: NSURL) {
    PHPhotoLibrary.requestAuthorization { status in
        // Return if unauthorized
        guard status == .Authorized else {
            print("Error saving video: unauthorized access")
            return
        }

        // If here, save video to library
        PHPhotoLibrary.sharedPhotoLibrary().performChanges({
            PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(videoURL)
        }) { success, error in
            if !success {
                print("Error saving video: \(error)")
            }
        }
    }
}

Điều này cần một cuộc gọi lại hoàn thành. Mặt khác, nó trả về trước khi viết xong. Tôi đã thay đổi điều đó, và nó hoạt động. Cảm ơn!
sudo


1
Tôi nhận được thông báo này "CGBitmapContextCreate: byte / hàng dữ liệu không hợp lệ: nên có ít nhất 13056 cho 8 bit / thành phần, 3 thành phần, kCGImageAlphaPremultipliedFirst." trong hàm fillPixelBufferFromImage: trong khi tạo CGBitmapContextCreate. Bất cứ ý tưởng tại sao điều này đang xảy ra ??
Linkon Sid

3
@oneyenjug Này anh bạn, cảm ơn vì điều đó, nhưng tôi gặp phải lỗi sau: "Lỗi khi chuyển đổi hình ảnh thành video: pixelBufferPool nil sau khi bắt đầu phiên". Bất cứ ý tưởng những gì tôi có thể làm sai?
Tim Vermeulen

1
Đối với "Lỗi chuyển đổi hình ảnh thành video: pixelBufferPool nil sau khi bắt đầu phiên" Với mã ở trên, bạn sẽ yêu cầu người dùng cho phép truy cập thư viện ảnh và tạo tệp, hơn là bạn sẽ thử lại và tệp tại đường dẫn sẽ được tạo, vì vậy hãy kiểm tra với Trình quản lý tệp và xóa tệp nếu nó tồn tại trước khi lưu nó.
Sam Bing

7

Đây là phiên bản swift3 cách chuyển đổi mảng Hình ảnh sang Video

import Foundation
import AVFoundation
import UIKit

typealias CXEMovieMakerCompletion = (URL) -> Void
typealias CXEMovieMakerUIImageExtractor = (AnyObject) -> UIImage?


public class ImagesToVideoUtils: NSObject {

    static let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
    static let tempPath = paths[0] + "/exprotvideo.mp4"
    static let fileURL = URL(fileURLWithPath: tempPath)
//    static let tempPath = NSTemporaryDirectory() + "/exprotvideo.mp4"
//    static let fileURL = URL(fileURLWithPath: tempPath)


    var assetWriter:AVAssetWriter!
    var writeInput:AVAssetWriterInput!
    var bufferAdapter:AVAssetWriterInputPixelBufferAdaptor!
    var videoSettings:[String : Any]!
    var frameTime:CMTime!
    //var fileURL:URL!

    var completionBlock: CXEMovieMakerCompletion?
    var movieMakerUIImageExtractor:CXEMovieMakerUIImageExtractor?


    public class func videoSettings(codec:String, width:Int, height:Int) -> [String: Any]{
        if(Int(width) % 16 != 0){
            print("warning: video settings width must be divisible by 16")
        }

        let videoSettings:[String: Any] = [AVVideoCodecKey: AVVideoCodecJPEG, //AVVideoCodecH264,
                                           AVVideoWidthKey: width,
                                           AVVideoHeightKey: height]

        return videoSettings
    }

    public init(videoSettings: [String: Any]) {
        super.init()


        if(FileManager.default.fileExists(atPath: ImagesToVideoUtils.tempPath)){
            guard (try? FileManager.default.removeItem(atPath: ImagesToVideoUtils.tempPath)) != nil else {
                print("remove path failed")
                return
            }
        }


        self.assetWriter = try! AVAssetWriter(url: ImagesToVideoUtils.fileURL, fileType: AVFileTypeQuickTimeMovie)

        self.videoSettings = videoSettings
        self.writeInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings)
        assert(self.assetWriter.canAdd(self.writeInput), "add failed")

        self.assetWriter.add(self.writeInput)
        let bufferAttributes:[String: Any] = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32ARGB)]
        self.bufferAdapter = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: self.writeInput, sourcePixelBufferAttributes: bufferAttributes)
        self.frameTime = CMTimeMake(1, 5)
    }

    func createMovieFrom(urls: [URL], withCompletion: @escaping CXEMovieMakerCompletion){
        self.createMovieFromSource(images: urls as [AnyObject], extractor:{(inputObject:AnyObject) ->UIImage? in
            return UIImage(data: try! Data(contentsOf: inputObject as! URL))}, withCompletion: withCompletion)
    }

    func createMovieFrom(images: [UIImage], withCompletion: @escaping CXEMovieMakerCompletion){
        self.createMovieFromSource(images: images, extractor: {(inputObject:AnyObject) -> UIImage? in
            return inputObject as? UIImage}, withCompletion: withCompletion)
    }

    func createMovieFromSource(images: [AnyObject], extractor: @escaping CXEMovieMakerUIImageExtractor, withCompletion: @escaping CXEMovieMakerCompletion){
        self.completionBlock = withCompletion

        self.assetWriter.startWriting()
        self.assetWriter.startSession(atSourceTime: kCMTimeZero)

        let mediaInputQueue = DispatchQueue(label: "mediaInputQueue")
        var i = 0
        let frameNumber = images.count

        self.writeInput.requestMediaDataWhenReady(on: mediaInputQueue){
            while(true){
                if(i >= frameNumber){
                    break
                }

                if (self.writeInput.isReadyForMoreMediaData){
                    var sampleBuffer:CVPixelBuffer?
                    autoreleasepool{
                        let img = extractor(images[i])
                        if img == nil{
                            i += 1
                            print("Warning: counld not extract one of the frames")
                            //continue
                        }
                        sampleBuffer = self.newPixelBufferFrom(cgImage: img!.cgImage!)
                    }
                    if (sampleBuffer != nil){
                        if(i == 0){
                            self.bufferAdapter.append(sampleBuffer!, withPresentationTime: kCMTimeZero)
                        }else{
                            let value = i - 1
                            let lastTime = CMTimeMake(Int64(value), self.frameTime.timescale)
                            let presentTime = CMTimeAdd(lastTime, self.frameTime)
                            self.bufferAdapter.append(sampleBuffer!, withPresentationTime: presentTime)
                        }
                        i = i + 1
                    }
                }
            }
            self.writeInput.markAsFinished()
            self.assetWriter.finishWriting {
                DispatchQueue.main.sync {
                    self.completionBlock!(ImagesToVideoUtils.fileURL)
                }
            }
        }
    }

    func newPixelBufferFrom(cgImage:CGImage) -> CVPixelBuffer?{
        let options:[String: Any] = [kCVPixelBufferCGImageCompatibilityKey as String: true, kCVPixelBufferCGBitmapContextCompatibilityKey as String: true]
        var pxbuffer:CVPixelBuffer?
        let frameWidth = self.videoSettings[AVVideoWidthKey] as! Int
        let frameHeight = self.videoSettings[AVVideoHeightKey] as! Int

        let status = CVPixelBufferCreate(kCFAllocatorDefault, frameWidth, frameHeight, kCVPixelFormatType_32ARGB, options as CFDictionary?, &pxbuffer)
        assert(status == kCVReturnSuccess && pxbuffer != nil, "newPixelBuffer failed")

        CVPixelBufferLockBaseAddress(pxbuffer!, CVPixelBufferLockFlags(rawValue: 0))
        let pxdata = CVPixelBufferGetBaseAddress(pxbuffer!)
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGContext(data: pxdata, width: frameWidth, height: frameHeight, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pxbuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
        assert(context != nil, "context is nil")

        context!.concatenate(CGAffineTransform.identity)
        context!.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
        CVPixelBufferUnlockBaseAddress(pxbuffer!, CVPixelBufferLockFlags(rawValue: 0))
        return pxbuffer
    }
}

Tôi sử dụng nó cùng với chụp màn hình, để cơ bản tạo ra một video chụp màn hình, đây là toàn bộ câu chuyện / ví dụ hoàn chỉnh .


Đây là mã Swift khá nghèo. Đó là một ví dụ hữu ích vì vậy tôi đã không đánh giá thấp, nhưng trong tương lai, đừng ép buộc (ngay cả khi bạn biết điều đó không thể thất bại, đó chỉ là một thói quen xấu và khiến cá tuyết khó đọc hơn), hãy sử dụng các tuyên bố bảo vệ và " let unsrappingValue = optValue "làm cho mã hợp lý hơn nhiều và rõ ràng là chính xác. Cũng đừng đặt parens xung quanh các điều kiện trong Swift. Và một số khoảng trắng các tham số / định nghĩa biến của bạn sẽ giúp dễ đọc, nhưng đó không phải là vấn đề Swift.
SafeFastExpressive

7

Chỉ cần dịch @Scott Raposa trả lời cho swift3 (với một số thay đổi rất nhỏ):

import AVFoundation
import UIKit
import Photos

struct RenderSettings {

    var size : CGSize = .zero
    var fps: Int32 = 6   // frames per second
    var avCodecKey = AVVideoCodecH264
    var videoFilename = "render"
    var videoFilenameExt = "mp4"


    var outputURL: URL {
        // Use the CachesDirectory so the rendered video file sticks around as long as we need it to.
        // Using the CachesDirectory ensures the file won't be included in a backup of the app.
        let fileManager = FileManager.default
        if let tmpDirURL = try? fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
            return tmpDirURL.appendingPathComponent(videoFilename).appendingPathExtension(videoFilenameExt)
        }
        fatalError("URLForDirectory() failed")
    }
}


class ImageAnimator {

    // Apple suggests a timescale of 600 because it's a multiple of standard video rates 24, 25, 30, 60 fps etc.
    static let kTimescale: Int32 = 600

    let settings: RenderSettings
    let videoWriter: VideoWriter
    var images: [UIImage]!

    var frameNum = 0

    class func saveToLibrary(videoURL: URL) {
        PHPhotoLibrary.requestAuthorization { status in
            guard status == .authorized else { return }

            PHPhotoLibrary.shared().performChanges({
                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
            }) { success, error in
                if !success {
                    print("Could not save video to photo library:", error)
                }
            }
        }
    }

    class func removeFileAtURL(fileURL: URL) {
        do {
            try FileManager.default.removeItem(atPath: fileURL.path)
        }
        catch _ as NSError {
            // Assume file doesn't exist.
        }
    }

    init(renderSettings: RenderSettings) {
        settings = renderSettings
        videoWriter = VideoWriter(renderSettings: settings)
//        images = loadImages()
    }

    func render(completion: (()->Void)?) {

        // The VideoWriter will fail if a file exists at the URL, so clear it out first.
        ImageAnimator.removeFileAtURL(fileURL: settings.outputURL)

        videoWriter.start()
        videoWriter.render(appendPixelBuffers: appendPixelBuffers) {
            ImageAnimator.saveToLibrary(videoURL: self.settings.outputURL)
            completion?()
        }

    }

//    // Replace this logic with your own.
//    func loadImages() -> [UIImage] {
//        var images = [UIImage]()
//        for index in 1...10 {
//            let filename = "\(index).jpg"
//            images.append(UIImage(named: filename)!)
//        }
//        return images
//    }

    // This is the callback function for VideoWriter.render()
    func appendPixelBuffers(writer: VideoWriter) -> Bool {

        let frameDuration = CMTimeMake(Int64(ImageAnimator.kTimescale / settings.fps), ImageAnimator.kTimescale)

        while !images.isEmpty {

            if writer.isReadyForData == false {
                // Inform writer we have more buffers to write.
                return false
            }

            let image = images.removeFirst()
            let presentationTime = CMTimeMultiply(frameDuration, Int32(frameNum))
            let success = videoWriter.addImage(image: image, withPresentationTime: presentationTime)
            if success == false {
                fatalError("addImage() failed")
            }

            frameNum += 1
        }

        // Inform writer all buffers have been written.
        return true
    }

}


class VideoWriter {

    let renderSettings: RenderSettings

    var videoWriter: AVAssetWriter!
    var videoWriterInput: AVAssetWriterInput!
    var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor!

    var isReadyForData: Bool {
        return videoWriterInput?.isReadyForMoreMediaData ?? false
    }

    class func pixelBufferFromImage(image: UIImage, pixelBufferPool: CVPixelBufferPool, size: CGSize) -> CVPixelBuffer {

        var pixelBufferOut: CVPixelBuffer?

        let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &pixelBufferOut)
        if status != kCVReturnSuccess {
            fatalError("CVPixelBufferPoolCreatePixelBuffer() failed")
        }

        let pixelBuffer = pixelBufferOut!

        CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))

        let data = CVPixelBufferGetBaseAddress(pixelBuffer)
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGContext(data: data, width: Int(size.width), height: Int(size.height),
                                bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)

        context!.clear(CGRect(x:0,y: 0,width: size.width,height: size.height))

        let horizontalRatio = size.width / image.size.width
        let verticalRatio = size.height / image.size.height
        //aspectRatio = max(horizontalRatio, verticalRatio) // ScaleAspectFill
        let aspectRatio = min(horizontalRatio, verticalRatio) // ScaleAspectFit

        let newSize = CGSize(width: image.size.width * aspectRatio, height: image.size.height * aspectRatio)

        let x = newSize.width < size.width ? (size.width - newSize.width) / 2 : 0
        let y = newSize.height < size.height ? (size.height - newSize.height) / 2 : 0

        context?.draw(image.cgImage!, in: CGRect(x:x,y: y, width: newSize.width, height: newSize.height))
        CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))

        return pixelBuffer
    }

    init(renderSettings: RenderSettings) {
        self.renderSettings = renderSettings
    }

    func start() {

        let avOutputSettings: [String: Any] = [
            AVVideoCodecKey: renderSettings.avCodecKey,
            AVVideoWidthKey: NSNumber(value: Float(renderSettings.size.width)),
            AVVideoHeightKey: NSNumber(value: Float(renderSettings.size.height))
        ]

        func createPixelBufferAdaptor() {
            let sourcePixelBufferAttributesDictionary = [
                kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32ARGB),
                kCVPixelBufferWidthKey as String: NSNumber(value: Float(renderSettings.size.width)),
                kCVPixelBufferHeightKey as String: NSNumber(value: Float(renderSettings.size.height))
            ]
            pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput,
                                                                      sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary)
        }

        func createAssetWriter(outputURL: URL) -> AVAssetWriter {
            guard let assetWriter = try? AVAssetWriter(outputURL: outputURL, fileType: AVFileTypeMPEG4) else {
                fatalError("AVAssetWriter() failed")
            }

            guard assetWriter.canApply(outputSettings: avOutputSettings, forMediaType: AVMediaTypeVideo) else {
                fatalError("canApplyOutputSettings() failed")
            }

            return assetWriter
        }

        videoWriter = createAssetWriter(outputURL: renderSettings.outputURL)
        videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: avOutputSettings)

        if videoWriter.canAdd(videoWriterInput) {
            videoWriter.add(videoWriterInput)
        }
        else {
            fatalError("canAddInput() returned false")
        }

        // The pixel buffer adaptor must be created before we start writing.
        createPixelBufferAdaptor()

        if videoWriter.startWriting() == false {
            fatalError("startWriting() failed")
        }

        videoWriter.startSession(atSourceTime: kCMTimeZero)

        precondition(pixelBufferAdaptor.pixelBufferPool != nil, "nil pixelBufferPool")
    }

    func render(appendPixelBuffers: ((VideoWriter)->Bool)?, completion: (()->Void)?) {

        precondition(videoWriter != nil, "Call start() to initialze the writer")

        let queue = DispatchQueue(label: "mediaInputQueue")
        videoWriterInput.requestMediaDataWhenReady(on: queue) {
            let isFinished = appendPixelBuffers?(self) ?? false
            if isFinished {
                self.videoWriterInput.markAsFinished()
                self.videoWriter.finishWriting() {
                    DispatchQueue.main.async {
                        completion?()
                    }
                }
            }
            else {
                // Fall through. The closure will be called again when the writer is ready.
            }
        }
    }

    func addImage(image: UIImage, withPresentationTime presentationTime: CMTime) -> Bool {

        precondition(pixelBufferAdaptor != nil, "Call start() to initialze the writer")

        let pixelBuffer = VideoWriter.pixelBufferFromImage(image: image, pixelBufferPool: pixelBufferAdaptor.pixelBufferPool!, size: renderSettings.size)
        return pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: presentationTime)
    }

}

2
Điều gì sẽ là một ví dụ về việc sử dụng?
Chewie The Chorkie

-7

Chà, điều này hơi khó thực hiện trong Objective-C thuần túy .... Nếu bạn đang phát triển cho các thiết bị đã bẻ khóa, một ý tưởng tốt là sử dụng công cụ dòng lệnh ffmpeg từ bên trong ứng dụng của bạn. Thật dễ dàng để tạo một bộ phim từ hình ảnh với một lệnh như:

ffmpeg -r 10 -b 1800 -i %03d.jpg test1800.mp4

Lưu ý rằng các hình ảnh phải được đặt tên tuần tự, và cũng được đặt trong cùng thư mục. Để biết thêm thông tin, hãy xem: http://electron.mit.edu/~gsteele/ffmpeg/


8
ffmpeg sẽ siêu chậm. tốt hơn để sử dụng các lớp AVFoundation tăng tốc phần cứng.
Fistman nhịp điệu

2
Không khó để làm, nó chỉ cần đọc tài liệu và viết mã. Một cách thích hợp hơn để phát triển ứng dụng hơn là yêu cầu người dùng tiềm năng của ứng dụng của bạn bẻ khóa điện thoại của họ và cài đặt ffmpeg.
Dave Durbin

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.