UIImage 배열을 동영상으로 내보내려면 어떻게합니까?


186

심각한 문제가 있습니다. NSArray여러 UIImage개체가 있습니다. 내가 지금하고 싶은 것은 그로부터 영화를 만드는 것 UIImages입니다. 그러나 나는 그렇게하는 방법을 모른다.

누군가 나를 도울 수 있기를 원하거나 내가 원하는 것과 같은 코드 스 니펫을 보내 주길 바랍니다.

편집 : 나중에 참조 할 수 있도록-솔루션을 적용한 후 비디오가 왜곡되어 보이는 경우 캡처하는 이미지 / 영역의 너비가 16의 배수인지 확인하십시오. 많은 시간이 걸리고 난 후 발견 :
UIImages에서 내 영화가 얻는 이유는 무엇입니까? 비뚤어진?

다음은 완전한 솔루션입니다 (너비가 16의 배수인지 확인 하십시오 )
http://codethink.no-ip.org/wordpress/archives/673


@ zoul : 태그는 가능한 해결책이 아니라 질문에 관한 내용을 포함해야합니다.
Georg Fritzsche

4
왜 안돼? 이미 AVFoundationFFmpeg에 대한 게시물이 있습니다 . AVFoundation 관련 정보를 찾고 있다면이 스레드를 보시겠습니까? (또는 메타의 합의인가?)
zoul

@zoul : 태그는 질문을 좁히고 ( "태그는 질문을 분류하는 키워드 또는 레이블입니다" ) 컨텍스트를 변경할 두 개를 추가합니다. 나는 이것이 명백하다고 생각했지만 메타에서 무언가에 대해 우연히 발견하면 알려 드리겠습니다. 또는 그곳에서 토론을 시작하십시오.
Georg Fritzsche

3
아마 누군가에게 유용 할 것입니다 -github의
SAKrisT

Dana는 없으며 Zoul 만 있습니다. [주제 외에 죄송하지만 저항 할 수 없습니다]
Rob VS

답변:


217

AVAssetWriter 및 나머지 AVFoundation 프레임 워크를 살펴보십시오 . 라이터에는 AVAssetWriterInput 유형의 입력이 있으며,이 스트림에는 appendSampleBuffer 라는 메소드 가있어 비디오 스트림에 개별 프레임을 추가 할 수 있습니다. 기본적으로 다음을 수행해야합니다.

1) 작가를 연결하십시오 :

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) 세션을 시작하십시오 :

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

3) 몇 가지 샘플을 작성하십시오.

// 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) 세션을 마치십시오 :

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

여전히 많은 공백을 채워야하지만 실제로 어려운 부분은 픽셀 버퍼를 얻는 것입니다 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;
}

frameSizeA는 CGSize기술 대상 프레임 크기와 frameTransformA는 CGAffineTransform당신이 프레임으로 그들을 그릴 때 이미지를 변환 할 수있다.


2
이것이 효과가 있지만 CGImage, CGBitmapContext뒷받침으로 끌어내는 것만으로 CVPixelBuffer낭비 하는 것은 낭비입니다. 대신 만드는 마찬가지로 CVPixelBuffer마다, AVAssetWriterInputPixelBufferAdaptor'S는 pixelBufferPool재생 버퍼로 사용되어야한다.
rpetrich

8
그러면 소스 데이터를 일반 이미지 파일로 가지고있을 때 어떻게해야합니까?
zoul

3
저는 zoul과 함께 있습니다. 데이터 소스가 많은 이미지로 구성되어 있으면 어떻게해야합니까? 이미지를 .mov로 인코딩하는 데 약 30 초 이하의 메모리 경고가 표시되고 메모리가 어디에 쌓여 있는지 알 수 없습니다
james

9
@rpetrich @zoul CMSampleBufferRef 대신 CVPixelBuffer 를 사용하는 이유 는 무엇 입니까? CMSampleBufferRef가 매개 변수 유형이 appendSampleBuffer:아닙니까? BTW, 저는 OS X에서 AVFoundation을 사용하고 있습니다.
Andrew Chang

12
이 의지 도움말 사람, 하루 stackoverflow.com/questions/9691646/...
DogCoffee

41

Objective-C의 iOS8에 대한 최신 작업 코드는 다음과 같습니다.

우리는 위의 @Zoul의 답변을 다양한 버전으로 조정하여 최신 버전의 Xcode 및 iOS8에서 작동하도록했습니다. 다음은 UIImage 배열을 가져 와서 .mov 파일로 만들어서 임시 디렉토리에 저장 한 다음 카메라 롤로 옮기는 완전한 작업 코드입니다. 이 작업을 수행하기 위해 여러 다른 게시물의 코드를 조립했습니다. 우리는 주석에서 코드를 작동시키기 위해 해결해야 할 함정을 강조했습니다.

(1) UIImage 모음 만들기

[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)];
}

이것은 AssetWriter를 생성하고 쓰기 위해 이미지를 추가하는 주요 방법입니다.

(2) 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) 쓰기 세션을 시작합니다 (참고 :이 방법은 위에서 계속 진행됨)

    //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) 세션 종료 (참고 : 방법은 위에서 계속)

                //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) UIImage를 CVPixelBufferRef로 변환
이 메소드는 AssetWriter에 필요한 CV 픽셀 버퍼 참조를 제공합니다. 이것은 UIImage (위)에서 가져온 CGImageRef에서 가져옵니다.

- (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) 비디오를 카메라 롤로 이동 AVAssetWriter는 카메라 롤에 직접 쓸 수 없기 때문에 "tmp / temp.mov"(또는 위에서 지정한 파일 이름)에서 비디오를 카메라 롤로 이동합니다.

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

Zoul의 대답은 당신이 무엇을 할 것인지에 대한 좋은 개요를 제공합니다. 우리는이 코드를 광범위하게 주석 처리하여 작업 코드를 사용하여 어떻게 수행되었는지 확인할 수 있습니다.


3
누구나 Swift 2.1에서 솔루션을 가질 수 있습니까?
Kyle KIM

2
누구나 Swift에 해결책이 있습니까?
Sam

2
@KyleKIM Swift에서 솔루션을 찾았습니까?
Sam

2
@Meseery 텍스트를 추가하려면 비디오에 추가 할 이미지에 텍스트를 추가하기 전에 텍스트를 작성하십시오. SO에서 이미지에 텍스트를 쓰는 방법에 대한 많은 예가 있습니다. 오디오의 경우 비디오 측면이 완료된 후 단계적으로 비디오에 추가 할 수 있습니다.
Praxiteles

3
누구나 메모리 문제에 대한 해결책이 있습니까? 약 3-4 백 개의 이미지에서 내 앱이 종료되었습니다. 나는 이미지 파일 경로의 배열을 전달하고 필요에 따라 각 이미지를로드하지만 메모리가 여전히 스택 보인다 해요
beebcon

31

참고 : 이것은 Swift 2.1 솔루션 (iOS8 +, XCode 7.2) 입니다.

지난주 나는 이미지에서 비디오를 생성하기 위해 iOS 코드를 작성하기로했다. AVFoundation 경험이 약간 있었지만 CVPixelBuffer에 대해 들어 본 적이 없습니다. 이 페이지와 여기 에 대한 답변을 보았습니다 . 모든 것을 해부하고 내 두뇌에 맞는 방식으로 Swift에 다시 모으는 데 며칠이 걸렸습니다. 아래는 내가 생각해 낸 것입니다.

참고 : 아래의 모든 코드를 단일 Swift 파일에 복사 / 붙여 넣기하면 컴파일해야합니다. 값 loadImages()RenderSettings값만 조정 하면 됩니다.

1 부 : 설정

여기에서는 모든 내보내기 관련 설정을 단일 RenderSettings구조체 로 그룹화합니다 .

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

2 부 : ImageAnimator

ImageAnimator클래스는 이미지에 대해 알고 및 사용 VideoWriter렌더링을 수행하는 클래스. 아이디어는 비디오 컨텐츠 ​​코드를 하위 레벨 AVFoundation 코드와 분리하여 유지하는 것입니다. 또한 saveToLibrary()체인의 끝에서 호출되어 비디오를 사진 라이브러리에 저장하는 클래스 함수로 여기에 추가 했습니다.

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
    }

}

3 부 : VideoWriter

VideoWriter클래스는 모든 AVFoundation 무거운 작업을 수행합니다. 주로 AVAssetWriter와 주위에 래퍼 AVAssetWriterInput입니다. 또한 이미지를로 변환하는 방법을 아는 저에 의해 작성된 멋진 코드가 포함되어 있습니다 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)
    }

}

제 4 부 : 실현 시키십시오

모든 것이 완료되면 다음 3 가지 마법 라인이 있습니다.

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

오, 언젠가 더 빨리 게시했으면 좋겠다. :)이 스레드에 추가 된 Swift 버전 포팅을 마쳤습니다. 라이브러리에 저장 기능을 통합했습니다.
Crashalot

@Crashalot-나중에 들려요! 작업 중이라는 느낌이 들었습니다. 그럼에도 불구하고 모든 것을 이해하고 다양한 비트를 나누는 데 좋은 연습이었습니다.
scootermg 2014 년

Crashalot 및 @Scott Raposa. 이것은이 질문과 관련이 있지만 특별한 테스트 사례입니다. 귀하의 코드는 절대적으로 경이롭고 훌륭하지만 images.count == 1 인 단일 이미지에서는 작동하지 않는 것 같습니다.이 문제를 해결하기 위해 코드를 변경했지만 엄청나게 어려운 것 같습니다. 너희들의 도움은 절대적으로 훌륭 할 것입니다. 또한 여기 stackoverflow.com/questions/38035809/… 에서 질문을했습니다 . 나는 images.count == 1의 특별한 경우가 해결되기를 바랐다! 감사!
impression7vx

@ impression7vx 현재 코드를 실행하고 있지 않으므로 해당 경우를 위해 특별히 테스트 할 수 없습니다. 그래도 왜 작동하지 않을지 잘 모르겠습니다. 작동하지 않는다고 말하면 무엇을보고 있습니까? 아마도 fps 설정으로 연주하십시오. 1로 가져 오십시오. 또는 2 개의 이미지로 작업하는 경우 이미지를 복제하십시오.
scootermg

하나의 이미지가 있다고 가정하면 지금 복제하고 작동합니다. 그러나 어떤 이유로 든 단일 이미지로는 작동하지 않습니다. 나는 단지 당신에게 알려줄 것이라고 생각했다! images = [image, image]작동하지만 단일 이미지는 사용 하지 않습니다. 시간을 10 초로 변경하면 마지막 프레임까지 검은 색 화면이 표시됩니다. 꽤 이상합니다.
impression7vx

21

나는 Zoul의 주요 아이디어를 취하여 AVAssetWriterInputPixelBufferAdaptor 메소드를 통합하고 그로부터 작은 프레임 워크의 시작을 만들었습니다.

자유롭게 확인하고 개선하십시오! CEMovieMaker


2
@CameronE 좋은 점이 있지만 한 가지 문제가 있습니다. 내 비디오가 1080 * 1920이면 어떻게 되나요? iPhone 5s, 6,6plus 후면 카메라 해상도,이 상황에서 비디오 엉망이 도와주세요.
Dipen Chudasama

1
안녕하세요, 동영상에 이미지가 표시되는 방식에서 속도를 어떻게 줄일 수 있는지 알려주세요.
히마

프레임 표시 시간 변경과 같이 비디오에 약간의 지연을 추가하는 방법은 무엇입니까?
ViruMax

13

다음은 iOS 8에서 테스트 된 Swift 2.x 버전입니다. @Scott Raposa 및 @Praxiteles의 답변과 다른 질문에 대한 @acj의 코드가 결합되어 있습니다. @acj에서 코드는 여기에 있습니다 : https://gist.github.com/acj/6ae90aa1ebb8cad6b47b . @TimBull도 코드를 제공했습니다.

@Scott Raposa와 마찬가지로 나는 CVPixelBufferPoolCreatePixelBuffer사용법을 이해 하지는 못했을 뿐 아니라 다른 여러 기능에 대해 들어 본 적이 없습니다 .

아래에서 보는 것은 주로 시행 착오와 Apple 문서를 읽음으로써 복잡해졌습니다. 주의해서 사용하고 실수가있는 경우 제안을 제공하십시오.

용법:

import UIKit
import AVFoundation
import Photos

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

암호:

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

완료 콜백이 필요합니다. 그렇지 않으면 쓰기가 완료되기 전에 반환됩니다. 나는 그것을 바꿨다. 감사!
sudo

1
맥 OS에 대한 스위프트 3 요점 - gist.github.com/isthisjoe/7f712512f6efd3f4d7500e98a7c48f8f
oneyenjug

1
"CGBitmapContextCreate : 유효하지 않은 데이터 바이트 / 행 : 8 개의 정수 비트 / 컴포넌트, 3 개의 컴포넌트, kCGImageAlphaPremultipliedFirst의 경우 13056 이상이어야합니다."라는 메시지가 표시됩니다. CGBitmapContextCreate를 작성하는 동안 fillPixelBufferFromImage 함수에서 : 왜 이런 일이 일어나는지 아십니까?
Linkon Sid

3
@oneyenjug 이봐, 고마워,하지만 다음과 같은 오류가 발생합니다 : "이미지를 비디오로 변환하는 동안 오류가 발생했습니다 : 세션을 시작한 후 pixelBufferPool nil". 내가 뭘 잘못 할 수 있는지 알아?
Tim Vermeulen

1
"이미지를 비디오로 변환하는 중 오류 발생 : 세션 시작 후 pixelBufferPool nil"위 코드를 사용하면 사용자가 다시 시도하는 것보다 사진 라이브러리에 액세스하고 파일을 작성할 수있는 권한을 요청합니다. 경로에있는 파일은 이미 파일을 저장하기 전에 FileManager로 확인하고 파일이 있으면 삭제하십시오.
Sam Bing 17

7

다음은 swift3 버전입니다. 이미지 배열을 비디오로 변환하는 방법

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

기본적으로 화면 캡처 비디오를 만들기 위해 화면 캡처와 함께 사용합니다 . 전체 스토리 / 완료 예제는 다음과 같습니다 .


이것은 매우 가난한 Swift 코드입니다. 유용한 예가되어 다운 보트를하지는 않았지만, 앞으로는 풀지 못하도록 강제하지 마십시오 (실패 할 수 없다는 것을 알고 있어도 나쁜 습관 일뿐 아니라 코드를 읽기 어렵게 만듭니다). unwrappedValue = optionalValue "로 설정하면 코드가 훨씬 더 합리적이고 확실해집니다. 또한 Swift에서 조건부 주위에 괄호를 넣지 마십시오. 그리고 매개 변수 / 변수 정의의 일부 공간은 가독성에 도움이되지만 Swift 문제는 ​​아닙니다.
SafeFastExpressive

7

@Scott Raposa 답변을 swift3으로 번역했습니다 (약간 변경 사항이 있음).

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
사용법의 예는 무엇입니까?
Chewie The Chorkie

-7

자, 이것은 순수한 Objective-C에서 구현하기가 약간 어렵습니다 .... 탈옥 장치를 위해 개발하는 경우 응용 프로그램 내부에서 명령 줄 도구 ffmpeg를 사용하는 것이 좋습니다. 다음과 같은 명령을 사용하여 이미지에서 영화를 만드는 것은 매우 쉽습니다.

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

이미지는 순차적으로 이름이 지정되어야하며 동일한 디렉토리에 배치되어야합니다. 자세한 내용은 http://electron.mit.edu/~gsteele/ffmpeg/ 를 참조하십시오 .


8
ffmpeg는 매우 느립니다. 하드웨어 가속 AVFoundation 클래스를 사용하는 것이 좋습니다.
리듬 피스트 맨

2
어렵지 않고 문서를 읽고 코드를 작성하기 만하면됩니다. 잠재적 인 앱 사용자가 휴대 전화를 탈옥하고 ffmpeg를 설치하도록 요구하는 것보다 훨씬 더 적절한 방법으로 앱을 개발할 수 있습니다.
Dave Durbin

-7

AVAssetWriter를 사용하여 이미지를 동영상으로 쓰십시오. 나는 이미 여기에 대답했다 : https : //.com/a/19166876/1582217


4
이것은 답이 아닙니다. 1) 문제에 대한 답변이없는 다른 게시물에 대한 링크입니다. 2) 실제로 답변이있는 경우 제공해야합니다.
chrisallick
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.