Bagaimana cara mengekspor array UIImage sebagai film?

186

Saya punya masalah serius: Saya punya NSArraybeberapa UIImagebenda. Apa yang sekarang ingin saya lakukan, adalah membuat film dari itu UIImages. Tetapi saya tidak tahu bagaimana melakukannya.

Saya harap seseorang dapat membantu saya atau mengirim saya potongan kode yang melakukan sesuatu seperti yang saya inginkan.

Sunting: Untuk referensi di masa mendatang - Setelah menerapkan solusi, jika video terlihat terdistorsi, pastikan lebar gambar / area yang Anda tangkap adalah kelipatan 16. Ditemukan setelah berjam-jam perjuangan di sini:
Mengapa film saya dari UIImages mendapat terdistorsi?

Ini adalah solusi lengkap (hanya pastikan lebar adalah kelipatan 16)
http://codethink.no-ip.org/wordpress/archives/673

Nuker
sumber
@ zoul: Tag harus mencakup tentang pertanyaannya, bukan solusi yang mungkin.
Georg Fritzsche
4
Kenapa tidak? Sudah ada pos untuk keduanya AVFoundationdan FFmpeg. Jika Anda mencari info terkait AVFoundation, tidakkah Anda ingin melihat utas ini? (Atau apakah itu konsensus dari Meta?)
zoul
@ zoul: Tag mempersempit pertanyaan ( "Tag adalah kata kunci atau label yang mengkategorikan pertanyaan Anda" ), dengan menambahkan kedua Anda akan mengubah konteks. Saya pikir ini sudah jelas, tetapi jika saya menemukan sesuatu tentang meta, saya akan memberi tahu Anda. Atau, mulailah diskusi di sana.
Georg Fritzsche
3
Mungkin akan berguna bagi seseorang - kode saya di github github.com/sakrist/One-minute
SAKrisT
Tidak ada Dana, hanya ada Zoul. [maaf untuk di luar topik, tapi saya tidak bisa menolak]
Rob VS

Jawaban:

217

Lihatlah AVAssetWriter dan sisa kerangka kerja AVFoundation . Penulis memiliki input tipe AVAssetWriterInput , yang pada gilirannya memiliki metode yang disebut appendSampleBuffer: yang memungkinkan Anda menambahkan frame individual ke aliran video. Pada dasarnya Anda harus:

1) Kirim penulis:

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) Mulai sesi:

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

3) Tulis beberapa sampel:

// 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) Selesaikan sesi:

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

Anda masih harus mengisi banyak bagian yang kosong, tetapi saya pikir satu-satunya bagian yang sangat sulit adalah mendapatkan penyangga piksel dari 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;
}

frameSizeadalah CGSizemenggambarkan ukuran bingkai target Anda dan frameTransformadalah CGAffineTransformyang memungkinkan Anda mengubah gambar saat Anda menggambarnya menjadi bingkai.

zoul
sumber
2
Meskipun ini berhasil, menggambar CGImagehanya untuk menarik itu ke CGBitmapContextdidukung oleh CVPixelBufferadalah sia-sia. Demikian pula, bukan menciptakan CVPixelBuffersetiap kali, AVAssetWriterInputPixelBufferAdaptor's pixelBufferPoolharus digunakan untuk buffer recycle.
rpetrich
8
Nah apa yang harus Anda lakukan kemudian, ketika Anda memiliki sumber data sebagai file gambar biasa?
zoul
3
Saya dengan zoul ... apa yang harus kita lakukan jika sumber data kita terdiri dari banyak gambar? saya mengalami peringatan memori sekitar 30 detik atau kurang dari penyandian gambar ke .mov dan saya tidak tahu di mana memori sedang membangun
james
9
@rpetrich @ zoul Bolehkah saya bertanya mengapa menggunakan CVPixelBuffer alih-alih CMSampleBufferRef ? Bukankah CMSampleBufferRef jenis parameter appendSampleBuffer:? BTW, saya menggunakan AVFoundation di OS X.
Andrew Chang
12
Ini akan membantu seseorang, suatu hari stackoverflow.com/questions/9691646/…
DogCoffee
41

Berikut adalah kode kerja terbaru pada iOS8 di Objective-C.

Kami harus membuat berbagai tweak untuk jawaban @ Zoul di atas untuk membuatnya berfungsi pada Xcode dan iOS8 versi terbaru. Berikut ini adalah kode kerja lengkap kami yang mengambil array UIImages, membuatnya menjadi file .mov, menyimpannya ke direktori temp, kemudian memindahkannya ke rol kamera. Kami mengumpulkan kode dari beberapa pos berbeda untuk membuatnya berfungsi. Kami telah menyoroti jebakan yang harus kami pecahkan untuk membuat kode berfungsi di komentar kami.

(1) Buat koleksi 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)];
}

Ini adalah metode utama yang membuat AssetWriter Anda dan menambahkan gambar untuk itu untuk ditulis.

(2) Sambungkan 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) Mulai Sesi penulisan (CATATAN: metode ini melanjutkan dari atas)

    //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) Selesaikan Sesi (Catatan: Metode berlanjut dari atas)

                //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) Ubah UIImages Anda menjadi CVPixelBufferRef
Metode ini akan memberi Anda referensi buffer piksel CV yang diperlukan oleh AssetWriter. Ini diperoleh dari CGImageRef yang Anda dapatkan dari UIImage Anda (di atas).

- (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) Pindahkan Video Anda ke Rol Kamera Karena AVAssetWriter tidak dapat menulis langsung ke rol kamera, ini memindahkan video dari "tmp / temp.mov" (atau nama file apa pun yang Anda beri nama di atas) ke rol kamera.

- (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];
    }
}

Jawaban Zoul di atas memberikan garis besar yang bagus tentang apa yang akan Anda lakukan. Kami banyak mengomentari kode ini sehingga Anda dapat melihat bagaimana hal itu dilakukan dengan menggunakan kode kerja.

Praxiteles
sumber
3
Adakah yang bisa memiliki solusi di Swift 2.1?
Kyle KIM
2
Adakah yang punya solusi di Swift?
Sam
2
@KyleKIM apakah Anda menemukan solusi di Swift?
Sam
2
@Meseery Untuk menambahkan teks, cukup tulis teks ke gambar yang Anda tambahkan ke video tetapi sebelum Anda menambahkannya. Ada banyak contoh cara menulis teks ke gambar di SO. Untuk audio, saya berharap Anda bisa menambahkannya ke video sebagai langkah setelah aspek video selesai.
Praxiteles
3
Adakah yang punya solusi untuk masalah memori? Sekitar 3 atau 4 ratus gambar, aplikasi saya terbunuh. Saya melewati array jalur file gambar dan memuat setiap gambar sesuai permintaan, tetapi tampaknya memori masih menumpuk
beebcon
31

CATATAN: Ini adalah solusi Swift 2.1 (iOS8 +, XCode 7.2) .

Minggu lalu saya mulai menulis kode iOS untuk menghasilkan video dari gambar. Saya memiliki sedikit pengalaman AVFoundation, tetapi belum pernah mendengar tentang CVPixelBuffer. Saya menemukan jawaban di halaman ini dan juga di sini . Butuh beberapa hari untuk membedah semuanya dan menyatukannya kembali di Swift dengan cara yang masuk akal bagi otak saya. Di bawah ini adalah apa yang saya pikirkan.

CATATAN: Jika Anda menyalin / menempelkan semua kode di bawah ini ke dalam file Swift tunggal, itu harus dikompilasi. Anda hanya perlu mengubah loadImages()dan RenderSettingsnilainya.

Bagian 1: Menyiapkan segalanya

Di sini saya mengelompokkan semua pengaturan terkait ekspor ke dalam satu RenderSettingsstruct.

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")
    }
}

Bagian 2: ImageAnimator

The ImageAnimatorkelas tahu tentang gambar dan menggunakan VideoWriterkelas untuk melakukan rendering. Idenya adalah untuk menjaga kode konten video terpisah dari kode AVFoundation tingkat rendah. Saya juga menambahkan di saveToLibrary()sini sebagai fungsi kelas yang dipanggil di akhir rantai untuk menyimpan video ke Perpustakaan Foto.

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
    }

}

Bagian 3: Penulis Video

The VideoWriterkelas melakukan semua angkat AVFoundation berat. Ini sebagian besar pembungkus di sekitar AVAssetWriterdan AVAssetWriterInput. Ini juga berisi kode mewah yang ditulis oleh bukan saya yang tahu bagaimana menerjemahkan gambar menjadi 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)
    }

}

Bagian 4: Wujudkan

Setelah semuanya ada, ini adalah 3 garis ajaib Anda:

let settings = RenderSettings()
let imageAnimator = ImageAnimator(renderSettings: settings)
imageAnimator.render() {
    print("yes")
}
scootermg
sumber
Oh man, semoga kamu telah memposting satu hari lebih cepat. :) Baru saja selesai porting versi Swift, ditambahkan ke utas ini. Masukkan fungsi save to library Anda, harap Anda tidak keberatan.
Crashalot
@ Crashalot - Saya dengar ya! Saya merasa sedang dikerjakan. Tetap saja, merupakan latihan yang baik dalam memahami semuanya dan memecah berbagai bit.
scootermg
Crashalot dan @Scott Raposa. Ini adalah komentar yang terkait dengan pertanyaan ini tetapi dalam kasus uji khusus. Kode Anda benar-benar fenomenal dan luar biasa, tetapi tampaknya tidak berfungsi untuk gambar TUNGGAL, di mana images.count == 1. Saya telah mengubah kode untuk mencoba dan mengatasi masalah ini, tetapi tampaknya sangat sulit. Setiap bantuan dari kalian akan sangat luar biasa. Saya juga mengajukan pertanyaan di sini di stackoverflow.com/questions/38035809/…… . Saya berharap kasus khusus images.count == 1 dapat diatasi! Terima kasih!
impression7vx
@ impression7vx Saya tidak memiliki kode yang berjalan saat ini sehingga saya tidak dapat mengujinya secara khusus untuk kasus itu. Saya tidak yakin mengapa itu tidak berhasil. Ketika Anda mengatakan itu tidak berhasil, apa yang Anda lihat? Mungkin bermain dengan pengaturan fps. Bawa ke 1. Atau jika Anda menggunakannya dengan 2 gambar, maka cukup duplikat gambar.
scootermg
Katakanlah saya memiliki 1 gambar, saat ini saya sedang menggandakan dan berfungsi. Tetapi untuk alasan apa pun itu tidak bekerja dengan satu gambar. Saya pikir saya hanya akan memberi tahu Anda! Menggunakan images = [image, image]karya tetapi satu gambar tidak. Katakanlah saya mengubah waktu ke 10detik, lalu saya melihat layar hitam hingga bingkai terakhir. Cukup aneh.
impression7vx
21

Saya mengambil ide-ide utama Zoul dan memasukkan metode AVAssetWriterInputPixelBufferAdaptor dan membuat awal dari kerangka kerja kecil dari itu.

Jangan ragu untuk memeriksanya dan memperbaikinya! CEMovieMaker

Cameron E
sumber
2
@CameronE Bagus tapi saya punya satu masalah, Bagaimana jika video saya 1080 * 1920? iPhone 5s, resolusi kamera belakang 6,6 plus, Video berantakan dalam situasi ini tolong bantu saya.
Dipen Chudasama
1
Hai, dapatkah Anda memberi tahu saya bagaimana cara mengatur kecepatan yang lebih rendah dalam cara gambar muncul di video?
Hima
Bagaimana cara menambahkan beberapa penundaan dalam video seperti mengubah waktu frame mana yang akan muncul?
ViruMax
13

Berikut ini adalah versi Swift 2.x yang diuji pada iOS 8. Ini menggabungkan jawaban dari @Scott Raposa dan @Praxiteles bersama dengan kode dari @acj berkontribusi untuk pertanyaan lain. Kode dari @acj ada di sini: https://gist.github.com/acj/6ae90aa1ebb8cad6b47b . @TimBull juga menyediakan kode.

Seperti @Scott Raposa, saya bahkan belum pernah mendengar CVPixelBufferPoolCreatePixelBufferdan beberapa fungsi lainnya, apalagi mengerti cara menggunakannya.

Apa yang Anda lihat di bawah dirakit sebagian besar oleh coba-coba dan dari membaca dokumen Apple. Harap gunakan dengan hati-hati, dan berikan saran jika ada kesalahan.

Pemakaian:

import UIKit
import AVFoundation
import Photos

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

Kode:

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)")
            }
        }
    }
}
Crashalot
sumber
Ini membutuhkan panggilan balik penyelesaian. Kalau tidak, ia kembali sebelum selesai menulis. Saya mengubahnya, dan itu berhasil. Terima kasih!
sudo
1
Gist for Swift 3 di macOS - gist.github.com/isthisjoe/7f712512f6efd3f4d7500e98a7c48f8f
oneyenjug
1
Saya mendapatkan pesan ini "CGBitmapContextCreate: byte / baris data tidak valid: harus setidaknya 13056 untuk 8 bit integer / komponen, 3 komponen, kCGImageAlphaPremultipliedFirst." dalam fungsi fillPixelBufferFromImage: saat membuat CGBitmapContextCreate. Tahu mengapa hal ini terjadi??
Linkon Sid
3
@oneyenjug Hai, terima kasih untuk itu, tapi saya mendapatkan kesalahan berikut: "Kesalahan mengonversi gambar ke video: pixelBufferPool nil setelah memulai sesi". Adakah yang tahu kesalahan saya?
Tim Vermeulen
1
Untuk "Kesalahan mengonversi gambar ke video: pixelBufferPool nil setelah memulai sesi" Dengan kode di atas Anda akan meminta izin kepada pengguna untuk mengakses perpustakaan foto dan membuat file, daripada Anda akan mencoba lagi dan file di jalur sudah akan dibuat, jadi periksa dengan FileManager dan hapus file tersebut jika ada sebelum menyimpannya.
Sam Bing
7

Inilah versi swift3 cara mengonversi array Gambar ke 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
    }
}

Saya menggunakannya bersama dengan screen capture, pada dasarnya membuat video screen capture, inilah kisah lengkap / contoh lengkapnya .

Mikita Manko
sumber
Ini adalah kode Swift yang sangat buruk. Ini adalah contoh yang berguna jadi saya tidak mengundurkan diri, tetapi di masa depan, jangan memaksa membuka (bahkan ketika Anda tahu itu tidak bisa gagal, itu hanya kebiasaan buruk dan membuat ikan cod lebih mudah dibaca), gunakan pernyataan penjaga, dan " biarkan unwrappedValue = optionalValue "membuat kode jauh lebih masuk akal dan jelas benar. Juga jangan menaruh parens di sekitar conditional di Swift. Dan beberapa spasi parameter / definisi variabel Anda akan membantu keterbacaan, tapi itu bukan masalah Swift.
SafeFastExpressive
7

Baru saja menerjemahkan @Scott Raposa jawaban ke swift3 (dengan sedikit perubahan):

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)
    }

}
Yedidya Reiss
sumber
2
Apa yang akan menjadi contoh penggunaan?
Chewie The Chorkie
-7

Nah ini agak sulit untuk diterapkan di Objective-C murni .... Jika Anda mengembangkan untuk perangkat yang sudah di-jailbreak, ide yang bagus adalah menggunakan alat command-line ffmpeg dari dalam aplikasi Anda. cukup mudah untuk membuat film dari gambar dengan perintah seperti:

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

Perhatikan bahwa gambar harus dinamai secara berurutan, dan juga ditempatkan di direktori yang sama. Untuk informasi lebih lanjut, lihat: http://electron.mit.edu/~gsteele/ffmpeg/

Kostas.N
sumber
8
ffmpeg akan menjadi sangat lambat. lebih baik menggunakan perangkat keras kelas AVFoundation yang dipercepat.
Fistman Berirama
2
Ini tidak sulit untuk dilakukan, hanya membutuhkan membaca dokumentasi dan menulis kode. Cara yang jauh lebih tepat untuk mengembangkan aplikasi daripada mengharuskan pengguna potensial aplikasi Anda untuk melakukan jailbreak pada ponsel mereka dan menginstal ffmpeg.
Dave Durbin
-7

Gunakan AVAssetWriter untuk menulis gambar sebagai film. Saya sudah menjawab di sini: - https://stackoverflow.com/a/19166876/1582217

Mohd Iftekhar Qurashi
sumber
4
Ini bukan jawabannya. 1) itu adalah tautan ke pos lain di mana Anda tidak menjawab masalah 2) jika Anda benar-benar memiliki jawaban, Anda harus memberikannya.
chrisallick