我正在更新一个64位兼容的应用程序,但我的电影录制代码有点困难。我们有一个FireWire相机,它将YUV帧提供给我们的应用程序,我们在MPEG4电影中处理和编码到磁盘。目前,我们使用基于C的QuickTime API来执行此操作(使用图像压缩管理器等),但旧的QuickTime API不支持64位。
我的第一次尝试是使用QTKit的QTMovie并使用-addImage:forDuration:withAttributes:
对单个帧进行编码,但这需要为每个帧创建一个NSImage(计算成本很高)并且does not do temporal compression,所以它不需要生成最紧凑的文件。
我想使用像QTKit Capture的QTCaptureMovieFileOutput这样的东西,但我无法弄清楚如何将原始帧提供给与QTCaptureInput无关的原始帧。由于需要手动控制增益,曝光等,我们无法直接使用QTKit Capture相机。
在Lion上,我们现在拥有AVFoundation中的AVAssetWriter类,可以让你这样做,但我仍然需要暂时定位Snow Leopard,所以我也想找到一个可以在那里工作的解决方案。
因此,有没有办法对视频进行非QuickTime逐帧录制,这种录制比QTMovie的-addImage:forDuration:withAttributes:
更高效,并且生成的文件大小可与旧版QuickTime API相媲美?
答案 0 :(得分:10)
最后,我决定采用TiansHUo建议的方法,并在此处使用libavcodec进行视频压缩。基于Martin here的指示,我downloaded the FFmpeg source并使用
构建了必要库的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 compliance instructions信函。
为了让这些共享库在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格式),并在movieFileName
文件中将它们编码为MP4容器中的MPEG4视频。代码如下:
<强> 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
这适用于64位应用程序中的Lion和Snow Leopard。它以与我之前基于QuickTime的方法相同的比特率记录,整体CPU使用率较低。
希望这会帮助处于类似情况的其他人。
答案 1 :(得分:5)
上个月我在WWDC问了一个类似QuickTime工程师的问题,他们基本上建议使用32位帮助程序... 我知道那不是你想听到的。 ;)
答案 2 :(得分:4)
是的,至少有一种方法可以进行非QuickTime逐帧录制视频,效率更高,并生成与Quicktime相当的文件。
开源库 libavcodec 非常适合您的视频编码。它用于非常流行的开源和商业软件和库(例如:mplayer,谷歌浏览器,imagemagick,opencv)它还提供了大量的选项来调整和多种文件格式(所有重要的格式和许多异国情调的格式)。它效率很高,可以生成各种比特率的文件。
来自维基百科:
libavcodec是一个免费软件/开源LGPL许可的库 用于编码和解码视频和音频数据的编解码器。[1]它是 由FFmpeg项目或Libav项目提供。[2] [3] libavcodec是一个 许多开源多媒体应用程序和组件的组成部分 构架。流行的MPlayer,xine和VLC媒体播放器使用它 它们的主要内置解码引擎,可以播放许多内容 所有支持的平台上的音频和视频格式。它也被使用 ffdshow试用解码器作为其主要解码库。 libavcodec还用于视频编辑和转码应用程序 像Avidemux,MEncoder或Kdenlive一样用于解码和编码。 libavcodec的特别之处在于它包含解码器,有时也包含解码器 几种专有格式的编码器实现,包括一些 没有公开发布的公开规范。这反过来了 因此,工程工作是libavcodec的重要组成部分 发展。在标准中提供此类编解码器 libavcodec框架比使用它提供了许多好处 原始编解码器,最显着的是增加了可移植性,在某些情况下 还有更好的性能,因为libavcodec包含一个标准库 高度优化的通用构建块实现,例如 DCT和色彩空间转换。但是,即使是libavcodec 努力进行与官方实施有点精确的解码, 有时这种重新实现中的错误和缺失的功能 引入播放某些文件的兼容性问题。
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,这意味着你必须提到你已经使用过它们,并且不需要开源你的项目。