记录视频帧到电影中的10.6兼容方法,不使用QuickTime API是什么?

13

我正在更新一个应用程序以实现64位兼容性,但是在录制电影的代码方面遇到了一些困难。我们有一个FireWire相机将YUV帧馈送到我们的应用程序,我们对其进行处理并在MPEG4电影中进行编码。目前,我们使用基于C的QuickTime API来实现这一点(使用图像压缩管理器等),但旧的QuickTime API不支持64位。

我的第一次尝试是使用QTKit的QTMovie,使用-addImage:forDuration:withAttributes:对单个帧进行编码,但这需要为每个帧创建NSImage(计算代价高),而且它不会进行时间压缩,因此不会生成最紧凑的文件。

我想使用类似QTKit Capture的QTCaptureMovieFileOutput,但我无法弄清楚如何将未与QTCaptureInput关联的原始帧馈送到该输出中。由于我们需要手动控制增益、曝光等设置,因此无法直接使用QTKit Capture来使用我们的相机。

在Lion上,我们现在在AVFoundation中拥有AVAssetWriter类,可以让您做到这一点,但是现在我仍需要针对Snow Leopard进行开发,因此我正在寻找一种在此版本上实现的解决方案。

因此,有没有一种比QTMovie的-addImage:forDuration:withAttributes:更高效的非QuickTime逐帧录制视频并且可以生成与旧的QuickTime API相当的文件大小?


哦,你设置了悬赏 - 这就解释了为什么你的声望又跌到了4万以下。至少你曾经突破了4万,我应该向你道贺 :) - BoltClock
1
@BoltClock - 不妨利用这些虚拟点数做些有意义的事情。上次我发布悬赏,结果得到了一个我不知道可能存在的解决方案。 - Brad Larson
@JaredUpdike - 最好的资源可能是苹果公司的CaptureAndCompressIPBMovie示例代码,它应该可以满足您的需求。除此之外,我现在能找到的只有这个关于压缩选项的问答:http://developer.apple.com/library/mac/#qa/qa1444/_index.html。他们可能已经删除了一些旧的文档,因为那些文档不兼容64位QT。 - Brad Larson
@Brad:那应该是一个不错的起点,演示运行得很好!谢谢。 - Jared Updike
@JaredUpdike - Lion中的AVAssetWriter类足以进行标准编码,如果它在Snow Leopard上能够正常工作,那对我来说就已经足够了。Mountain Lion似乎增加了从旧的QT方法转换中缺失的其余功能。不过,与QT相比,libavcodec在编码时为我提供了更好质量的视频,并且CPU使用率更低,所以我对该库的使用感到非常满意。 - Brad Larson
显示剩余2条评论
3个回答

10
最终,我决定采用TiansHUo建议的方法,在这里使用libavcodec进行视频压缩。根据Martin在此处的说明,我下载了FFmpeg源代码并构建了必要库的64位兼容版本。
./configure --disable-gpl --arch=x86_64 --cpu=core2 --enable-shared --disable-amd3dnow --enable-memalign-hack --cc=llvm-gcc
make
sudo make install

这将为Mac上的64位Core2处理器创建LGPL共享库。很遗憾,我还没有找到一种方法使库在启用MMX优化时不会崩溃,因此现在已禁用。这会使编码速度有所下降。经过一些实验,我发现可以使用上述配置选项构建64位版本的库,该库启用了MMX优化,在Mac上稳定运行。与禁用MMX的库相比,这样编码速度要快得多。
请注意,如果您使用这些共享库,应确保严格按照FFmpeg网站上的LGPL合规说明执行。
为了使这些共享库在正确的文件夹中放置并在我的Mac应用程序包中正常工作,我需要使用install_name_tool来调整这些库中的内部搜索路径,以指向它们在应用程序包中Frameworks目录中的新位置。
install_name_tool -id @executable_path/../Frameworks/libavutil.51.9.1.dylib libavutil.51.9.1.dylib

install_name_tool -id @executable_path/../Frameworks/libavcodec.53.7.0.dylib libavcodec.53.7.0.dylib
install_name_tool -change /usr/local/lib/libavutil.dylib @executable_path/../Frameworks/libavutil.51.9.1.dylib libavcodec.53.7.0.dylib

install_name_tool -id @executable_path/../Frameworks/libavformat.53.4.0.dylib libavformat.53.4.0.dylib
install_name_tool -change /usr/local/lib/libavutil.dylib @executable_path/../Frameworks/libavutil.51.9.1.dylib libavformat.53.4.0.dylib
install_name_tool -change /usr/local/lib/libavcodec.dylib @executable_path/../Frameworks/libavcodec.53.7.0.dylib libavformat.53.4.0.dylib

install_name_tool -id @executable_path/../Frameworks/libswscale.2.0.0.dylib libswscale.2.0.0.dylib
install_name_tool -change /usr/local/lib/libavutil.dylib @executable_path/../Frameworks/libavutil.51.9.1.dylib libswscale.2.0.0.dylib

你的特定路径可能会有所不同。这种调整使它们能够在应用程序包内工作,而无需在用户系统上安装它们在 /usr/local/lib 中。
然后,我让我的 Xcode 项目链接到这些库,并创建了一个独立的类来处理视频编码。该类通过 videoFrameToEncode 属性接收原始视频帧(以 BGRA 格式),并将它们编码为 MPEG4 视频并保存在 MP4 容器中的 movieFileName 文件中。代码如下:

SPVideoRecorder.h

#import <Foundation/Foundation.h>

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"

uint64_t getNanoseconds(void);

@interface SPVideoRecorder : NSObject
{
    NSString *movieFileName;
    CGFloat framesPerSecond;
    AVCodecContext *codecContext;
    AVStream *videoStream;
    AVOutputFormat *outputFormat;
    AVFormatContext *outputFormatContext;
    AVFrame *videoFrame;
    AVPicture inputRGBAFrame;

    uint8_t *pictureBuffer;
    uint8_t *outputBuffer;
    unsigned int outputBufferSize;
    int frameColorCounter;

    unsigned char *videoFrameToEncode;

    dispatch_queue_t videoRecordingQueue;
    dispatch_semaphore_t frameEncodingSemaphore;
    uint64_t movieStartTime;
}

@property(readwrite, assign) CGFloat framesPerSecond;
@property(readwrite, assign) unsigned char *videoFrameToEncode;
@property(readwrite, copy) NSString *movieFileName;

// Movie recording control
- (void)startRecordingMovie;
- (void)encodeNewFrameToMovie;
- (void)stopRecordingMovie;


@end

SPVideoRecorder.m

#import "SPVideoRecorder.h"
#include <sys/time.h>

@implementation SPVideoRecorder

uint64_t getNanoseconds(void)
{
    struct timeval now;
    gettimeofday(&now, NULL);
    return now.tv_sec * NSEC_PER_SEC + now.tv_usec * NSEC_PER_USEC;
}

#pragma mark -
#pragma mark Initialization and teardown

- (id)init;
{
    if (!(self = [super init]))
    {
        return nil;     
    }

    /* must be called before using avcodec lib */
    avcodec_init();

    /* register all the codecs */
    avcodec_register_all();
    av_register_all();

    av_log_set_level( AV_LOG_ERROR );

    videoRecordingQueue = dispatch_queue_create("com.sonoplot.videoRecordingQueue", NULL);;
    frameEncodingSemaphore = dispatch_semaphore_create(1);

    return self;
}

#pragma mark -
#pragma mark Movie recording control

- (void)startRecordingMovie;
{   
    dispatch_async(videoRecordingQueue, ^{
        NSLog(@"Start recording to file: %@", movieFileName);

        const char *filename = [movieFileName UTF8String];

        // Use an MP4 container, in the standard QuickTime format so it's readable on the Mac
        outputFormat = av_guess_format("mov", NULL, NULL);
        if (!outputFormat) {
            NSLog(@"Could not set output format");
        }

        outputFormatContext = avformat_alloc_context();
        if (!outputFormatContext)
        {
            NSLog(@"avformat_alloc_context Error!");
        }

        outputFormatContext->oformat = outputFormat;
        snprintf(outputFormatContext->filename, sizeof(outputFormatContext->filename), "%s", filename);

        // Add a video stream to the MP4 file 
        videoStream = av_new_stream(outputFormatContext,0);
        if (!videoStream)
        {
            NSLog(@"av_new_stream Error!");
        }


        // Use the MPEG4 encoder (other DiVX-style encoders aren't compatible with this container, and x264 is GPL-licensed)
        AVCodec *codec = avcodec_find_encoder(CODEC_ID_MPEG4);  
        if (!codec) {
            fprintf(stderr, "codec not found\n");
            exit(1);
        }

        codecContext = videoStream->codec;

        codecContext->codec_id = codec->id;
        codecContext->codec_type = AVMEDIA_TYPE_VIDEO;
        codecContext->bit_rate = 4800000;
        codecContext->width = 640;
        codecContext->height = 480;
        codecContext->pix_fmt = PIX_FMT_YUV420P;
//      codecContext->time_base = (AVRational){1,(int)round(framesPerSecond)};
//      videoStream->time_base = (AVRational){1,(int)round(framesPerSecond)};
        codecContext->time_base = (AVRational){1,200}; // Set it to 200 FPS so that we give a little wiggle room when recording at 50 FPS
        videoStream->time_base = (AVRational){1,200};
//      codecContext->max_b_frames = 3;
//      codecContext->b_frame_strategy = 1;
        codecContext->qmin = 1;
        codecContext->qmax = 10;    
//      codecContext->mb_decision = 2; // -mbd 2
//      codecContext->me_cmp = 2; // -cmp 2
//      codecContext->me_sub_cmp = 2; // -subcmp 2
        codecContext->keyint_min = (int)round(framesPerSecond); 
//      codecContext->flags |= CODEC_FLAG_4MV; // 4mv
//      codecContext->flags |= CODEC_FLAG_LOOP_FILTER;
        codecContext->i_quant_factor = 0.71;
        codecContext->qcompress = 0.6;
//      codecContext->max_qdiff = 4;
        codecContext->flags2 |= CODEC_FLAG2_FASTPSKIP;

        if(outputFormat->flags & AVFMT_GLOBALHEADER)
        {
            codecContext->flags |= CODEC_FLAG_GLOBAL_HEADER;
        }

        // Open the codec
        if (avcodec_open(codecContext, codec) < 0) 
        {
            NSLog(@"Couldn't initialize the codec");
            return;
        }

        // Open the file for recording
        if (avio_open(&outputFormatContext->pb, outputFormatContext->filename, AVIO_FLAG_WRITE) < 0) 
        { 
            NSLog(@"Couldn't open file");
            return;
        } 

        // Start by writing the video header
        if (avformat_write_header(outputFormatContext, NULL) < 0) 
        { 
            NSLog(@"Couldn't write video header");
            return;
        } 

        // Set up the video frame and output buffers
        outputBufferSize = 400000;
        outputBuffer = malloc(outputBufferSize);
        int size = codecContext->width * codecContext->height;

        int pictureBytes = avpicture_get_size(PIX_FMT_YUV420P, codecContext->width, codecContext->height);
        pictureBuffer = (uint8_t *)av_malloc(pictureBytes);

        videoFrame = avcodec_alloc_frame();
        videoFrame->data[0] = pictureBuffer;
        videoFrame->data[1] = videoFrame->data[0] + size;
        videoFrame->data[2] = videoFrame->data[1] + size / 4;
        videoFrame->linesize[0] = codecContext->width;
        videoFrame->linesize[1] = codecContext->width / 2;
        videoFrame->linesize[2] = codecContext->width / 2;

        avpicture_alloc(&inputRGBAFrame, PIX_FMT_BGRA, codecContext->width, codecContext->height);

        frameColorCounter = 0;

        movieStartTime = getNanoseconds();
    });
}

- (void)encodeNewFrameToMovie;
{
//  NSLog(@"Encode frame");

    if (dispatch_semaphore_wait(frameEncodingSemaphore, DISPATCH_TIME_NOW) != 0)
    {
        return;
    }

    dispatch_async(videoRecordingQueue, ^{
//      CFTimeInterval previousTimestamp = CFAbsoluteTimeGetCurrent();
        frameColorCounter++;

        if (codecContext == NULL)
        {       
            return;
        }

        // Take the input BGRA texture data and convert it to a YUV 4:2:0 planar frame
        avpicture_fill(&inputRGBAFrame, videoFrameToEncode, PIX_FMT_BGRA, codecContext->width, codecContext->height);
        struct SwsContext * img_convert_ctx = sws_getContext(codecContext->width, codecContext->height, PIX_FMT_BGRA, codecContext->width, codecContext->height, PIX_FMT_YUV420P, SWS_FAST_BILINEAR, NULL, NULL, NULL); 
        sws_scale(img_convert_ctx, (const uint8_t* const *)inputRGBAFrame.data, inputRGBAFrame.linesize, 0, codecContext->height, videoFrame->data, videoFrame->linesize);

        // Encode the frame
        int out_size = avcodec_encode_video(codecContext, outputBuffer, outputBufferSize, videoFrame);  

        // Generate a packet and insert in the video stream
        if (out_size != 0) 
        {
            AVPacket videoPacket;
            av_init_packet(&videoPacket);

            if (codecContext->coded_frame->pts != AV_NOPTS_VALUE) 
            {
                uint64_t currentFrameTime = getNanoseconds();

                videoPacket.pts = av_rescale_q(((uint64_t)currentFrameTime - (uint64_t)movieStartTime) / 1000ull/*codecContext->coded_frame->pts*/, AV_TIME_BASE_Q/*codecContext->time_base*/, videoStream->time_base);

//              NSLog(@"Frame time %lld, converted time: %lld", ((uint64_t)currentFrameTime - (uint64_t)movieStartTime) / 1000ull, videoPacket.pts);
            }

            if(codecContext->coded_frame->key_frame)
            {
                videoPacket.flags |= AV_PKT_FLAG_KEY;
            }
            videoPacket.stream_index = videoStream->index;
            videoPacket.data = outputBuffer;
            videoPacket.size = out_size;

            int ret = av_write_frame(outputFormatContext, &videoPacket);
            if (ret < 0) 
            { 
                av_log(outputFormatContext, AV_LOG_ERROR, "%s","Error while writing frame.\n"); 
                av_free_packet(&videoPacket);
                return;
            } 

            av_free_packet(&videoPacket);
        }

//      CFTimeInterval frameDuration = CFAbsoluteTimeGetCurrent() - previousTimestamp;
//      NSLog(@"Frame duration: %f ms", frameDuration * 1000.0);

        dispatch_semaphore_signal(frameEncodingSemaphore);
    });
}

- (void)stopRecordingMovie;
{
    dispatch_async(videoRecordingQueue, ^{
        // Write out the video trailer
        if (av_write_trailer(outputFormatContext) < 0) 
        { 
            av_log(outputFormatContext, AV_LOG_ERROR, "%s","Error while writing trailer.\n"); 
            exit(1); 
        } 

        // Close out the file
        if (!(outputFormat->flags & AVFMT_NOFILE)) 
        {
            avio_close(outputFormatContext->pb);
        }

        // Free up all movie-related resources
        avcodec_close(codecContext);
        av_free(codecContext);
        codecContext = NULL;

        free(pictureBuffer);
        free(outputBuffer);

        av_free(videoFrame);
        av_free(outputFormatContext);
        av_free(videoStream);       
    });

}

#pragma mark -
#pragma mark Accessors

@synthesize framesPerSecond, videoFrameToEncode, movieFileName;

@end

这在Lion和Snow Leopard的64位应用程序下工作。它以与我之前基于QuickTime的方法相同的比特率记录,但CPU使用率更低。

希望这能帮助到其他遇到类似情况的人。


5

上个月在WWDC上,我向QuickTime工程师提出了一个非常类似的问题,他们基本上建议使用32位辅助进程......我知道这不是你想听到的。;)

希望以上翻译能够帮到您。

4

是的,至少有一种方法可以进行非QuickTime逐帧录制视频,这种方法更有效,并且生成的文件与Quicktime可比。

开源库libavcodec非常适合您的视频编码情况。它被广泛用于流行的开源和商业软件和库中(例如:mplayer、Google Chrome、ImageMagick、OpenCV),并提供大量选项进行微调和众多文件格式(所有重要格式和许多奇特的格式)。它高效并以各种比特率生成文件。

来自维基百科:

libavcodec是一个自由软件/开源LGPL许可的编解码视频和音频数据的库。它由FFmpeg项目或Libav项目提供。libavcodec是许多开源多媒体应用程序和框架的重要部分。流行的MPlayer、xine和VLC媒体播放器将其作为主要的内置解码引擎,使其能够在所有支持的平台上播放许多音频和视频格式。ffdshow tryouts解码器也将其用作其主要解码库。libavcodec还用于视频编辑和转码应用程序,如Avidemux、MEncoder或Kdenlive,用于解码和编码。libavcodec特别之处在于,它包含对几种专有格式的解码器和有时也包括编码器实现,其中包括尚未发布公共规范的格式。这种反向工程的努力因此成为libavcodec开发的重要部分。在标准的libavcodec框架中具有这些编解码器可以比使用原始编解码器带来许多好处,特别是增加了可移植性,并且在某些情况下还可以获得更好的性能,因为libavcodec包含高度优化的常见构建块(如DCT和颜色空间转换)的标准库实现。然而,即使libavcodec力求对官方实现进行比特精确的解码,这种重新实现中的错误和缺失功能有时也会引入兼容性问题,导致无法播放某些文件。
  • 您可以选择将FFmpeg直接导入到XCode项目中。
  • 另一种解决方案是将帧直接传输到FFmpeg可执行文件中。

FFmpeg项目是一个快速、准确的多媒体转码器,在OS X上可以应用于各种场景。


FFmpeg(包括libavcodec)可以在Mac上编译。
http://jungels.net/articles/ffmpeg-howto.html

FFmpeg(包括libavcodec)也可以在雪豹系统的64位上编译。
http://www.martinlos.com/?p=41

FFmpeg支持大量视频和音频编解码器:
http://en.wikipedia.org/wiki/Libavcodec#Implemented_video_codecs

请注意,libavcodec和FFmpeg是LGPL许可证,这意味着您必须声明您使用了它们,并且您不需要开源您的项目。


libavcodec是我们一段时间前评估过的东西,但最初被拒绝了,因为我们想要减少对第三方库的外部依赖以及由于许可证方面的担忧(请参见此处,了解满足FFmpeg开发人员在这方面有多么困难:https://roundup.libav.org/issue726)。然而,这可能是我们留下来完成此任务的最可行选项。 - Brad Larson
@Brad Larson,我看到他们的问题最终得到了解决,所以FFmpeg开发人员应该是合理的。我不熟悉法律方面的事情。 - TiansHUo
不,那个问题仍然存在,而且Chroma仍然被列入他们的“耻辱榜”,这只是显示了一些项目成员可能会有多么不合理。请注意,他们所攻击的项目是由FFmpeg贡献者运营的。 - Brad Larson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接