Skip to content

Instantly share code, notes, and snippets.

@g-l-i-t-c-h-o-r-s-e
Created November 24, 2025 19:26
Show Gist options
  • Select an option

  • Save g-l-i-t-c-h-o-r-s-e/3d58e3779a6aef79c719d0ee40e07617 to your computer and use it in GitHub Desktop.

Select an option

Save g-l-i-t-c-h-o-r-s-e/3d58e3779a6aef79c719d0ee40e07617 to your computer and use it in GitHub Desktop.
Export QC Image as Video with FFmpeg in Quartz Composer
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
# --- config ---
NAME="FFExport"
CLASS="FFExportPlugIn"
SRC="${SRC:-${CLASS}.m}"
PLUG="$NAME.plugin"
OUT="$(pwd)/build-manual"
INST="$HOME/Library/Graphics/Quartz Composer Plug-Ins"
XCODE_APP="${XCODE_APP:-/Applications/Xcode_9.4.1.app}"
DEV="$XCODE_APP/Contents/Developer"
SDKDIR="$DEV/Platforms/MacOSX.platform/Developer/SDKs"
SDK="${SDK:-}"
if [[ -z "${SDK}" ]]; then
if [[ -d "$SDKDIR/MacOSX10.14.sdk" ]]; then SDK="$SDKDIR/MacOSX10.14.sdk"
elif [[ -d "$SDKDIR/MacOSX10.13.sdk" ]]; then SDK="$SDKDIR/MacOSX10.13.sdk"
else SDK="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
fi
fi
[[ -d "$DEV" ]] || { echo "Xcode not found: $XCODE_APP"; exit 1; }
[[ -f "$SRC" ]] || { echo "Source not found: $SRC"; exit 1; }
[[ -n "$SDK" && -d "$SDK" ]] || { echo "macOS SDK not found."; exit 1; }
export DEVELOPER_DIR="$DEV"
# --- FFmpeg via MacPorts pkg-config ---
PKGCFG="/opt/local/bin/pkg-config"
[[ -x "$PKGCFG" ]] || { echo "pkg-config not found at $PKGCFG (install via MacPorts)"; exit 1; }
PKG_LIBS=(libavformat libavcodec libavutil libswscale)
CFLAGS_FFMPEG="$("$PKGCFG" --cflags "${PKG_LIBS[@]}")"
LIBS_FFMPEG="$("$PKGCFG" --libs "${PKG_LIBS[@]}")"
echo "Using SDK: $SDK"
rm -rf "$OUT"
mkdir -p "$OUT/x86_64" "$OUT/universal/$PLUG/Contents/MacOS" "$OUT/universal/$PLUG/Contents/Frameworks"
FRAMEWORKS="$OUT/universal/$PLUG/Contents/Frameworks"
if [[ -d "$INST/$PLUG" ]]; then
echo "Removing installed $INST/$PLUG"
rm -rf "$INST/$PLUG"
fi
COMMON_CFLAGS=(
-bundle -fobjc-arc -fobjc-link-runtime
-isysroot "$SDK"
-mmacosx-version-min=10.9
-I .
-I /opt/local/include
)
COMMON_LIBS=(
-framework Foundation
-framework Quartz
-framework OpenGL
-framework CoreGraphics
)
echo "Compiling x86_64 (FFmpeg export)…"
clang -arch x86_64 \
"${COMMON_CFLAGS[@]}" \
$CFLAGS_FFMPEG \
"$SRC" \
"${COMMON_LIBS[@]}" \
$LIBS_FFMPEG \
-o "$OUT/x86_64/$NAME"
# Layout bundle
cp -a "$OUT/x86_64/$NAME" "$OUT/universal/$PLUG/Contents/MacOS/$NAME"
# Info.plist
cat >"$OUT/universal/$PLUG/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleDevelopmentRegion</key> <string>English</string>
<key>CFBundleExecutable</key> <string>${NAME}</string>
<key>CFBundleIdentifier</key> <string>com.yourdomain.${NAME}</string>
<key>CFBundleInfoDictionaryVersion</key> <string>6.0</string>
<key>CFBundleName</key> <string>${NAME}</string>
<key>CFBundlePackageType</key> <string>BNDL</string>
<key>CFBundleShortVersionString</key> <string>1.0</string>
<key>CFBundleSupportedPlatforms</key> <array><string>MacOSX</string></array>
<key>CFBundleVersion</key> <string>1</string>
<key>QCPlugInClasses</key>
<array>
<string>${CLASS}</string>
</array>
<key>NSPrincipalClass</key> <string>QCPlugIn</string>
</dict></plist>
PLIST
# --- helpers for embedding FFmpeg dylibs from /opt/local/lib ---
mk_short_symlink_if_needed() {
local base="$1"
if [[ "$base" =~ ^(lib[^.]+)\.([0-9]+)\.[0-9.]+\.dylib$ ]]; then
local short="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.dylib"
if [[ ! -e "$FRAMEWORKS/$short" ]]; then
( cd "$FRAMEWORKS" && ln -s "$base" "$short" )
fi
fi
}
list_opt_local_deps() {
otool -L "$1" | awk '$1 ~ /^\/opt\/local\/lib\// {print $1}'
}
copy_and_rewrite() {
local src="$1"; [[ "$src" == /opt/local/lib/* ]] || return 0
local base dest; base="$(basename "$src")"; dest="$FRAMEWORKS/$base"
if [[ ! -f "$dest" ]]; then
echo " → Copy $base"
rsync -aL "$src" "$dest"
chmod u+w "$dest"
install_name_tool -id "@loader_path/$base" "$dest"
mk_short_symlink_if_needed "$base"
while IFS= read -r dep; do
local depbase; depbase="$(basename "$dep")"
copy_and_rewrite "$dep"
install_name_tool -change "$dep" "@loader_path/$depbase" "$dest"
done < <(list_opt_local_deps "$dest")
fi
}
seed_from_otool() {
local bin="$1"
while IFS= read -r path; do copy_and_rewrite "$path"; done < <(
otool -L "$bin" | awk '$1 ~ /^\/opt\/local\/lib\/lib(avformat|avcodec|avutil|swscale).*\.dylib$/ {print $1}'
)
}
seed_from_pkgconfig() {
for pc in "${PKG_LIBS[@]}"; do
local libdir; libdir="$("$PKGCFG" --variable=libdir "$pc" 2>/dev/null || echo /opt/local/lib)"
local cand
for cand in "$libdir/${pc}".*.dylib "$libdir/${pc}.dylib"; do
[[ -f "$cand" ]] && { copy_and_rewrite "$cand"; break; }
done
done
}
final_full_sweep() {
for lib in "$FRAMEWORKS"/*.dylib; do
while IFS= read -r dep; do
local depbase; depbase="$(basename "$dep")"
copy_and_rewrite "$dep"
install_name_tool -change "$dep" "@loader_path/$depbase" "$lib"
done < <(list_opt_local_deps "$lib")
done
}
echo "Embedding FFmpeg dylibs…"
BIN="$OUT/universal/$PLUG/Contents/MacOS/$NAME"
seed_from_otool "$BIN"
if ! compgen -G "$FRAMEWORKS/*.dylib" >/dev/null; then
seed_from_pkgconfig
fi
while IFS= read -r dep; do
base="$(basename "$dep")"
if [[ ! -e "$FRAMEWORKS/$base" ]]; then
stem="${base%.dylib}"
stem="${stem%.*}"
match=( "$FRAMEWORKS/$stem".*.dylib )
if [[ -e "${match[0]}" ]]; then
mk_short_symlink_if_needed "$(basename "${match[0]}")"
else
copy_and_rewrite "$dep"
fi
fi
install_name_tool -change "$dep" "@loader_path/../Frameworks/$base" "$BIN"
done < <(list_opt_local_deps "$BIN")
final_full_sweep
echo "Codesigning bundled libs…"
for lib in "$FRAMEWORKS"/*.dylib; do
codesign --force -s - "$lib" >/dev/null || true
done
codesign --force -s - "$OUT/universal/$PLUG" >/dev/null || true
echo "Installing to: $INST"
mkdir -p "$INST"
rsync -a "$OUT/universal/$PLUG" "$INST/"
echo "Verifying install…"
IBIN="$INST/$PLUG/Contents/MacOS/$NAME"
leaks=0
if otool -L "$IBIN" | awk '$1 ~ /^\/opt\/local\/lib\//' | grep -q .; then
echo "❌ main binary still references /opt/local/lib:"
otool -L "$IBIN" | awk '$1 ~ /^\/opt\/local\/lib\// {print " " $1}'
leaks=1
fi
for lib in "$INST/$PLUG/Contents/Frameworks/"*.dylib; do
if otool -L "$lib" | awk '$1 ~ /^\/opt\/local\/lib\//' | grep -q .; then
echo "❌ $(basename "$lib") still references /opt/local/lib:"
otool -L "$lib" | awk '$1 ~ /^\/opt\/local\/lib\// {print " " $1}'
leaks=1
fi
done
if [[ $leaks -ne 0 ]]; then
echo "Fixup failed; see above offending paths."
exit 1
fi
echo "Installed: $INST/$PLUG"
echo "Embedded libs:"
ls -1 "$INST/$PLUG/Contents/Frameworks" || true
echo "Relaunch Quartz Composer and look for 'FFExport (x86_64)'."
VUILD
// FFExportPlugIn.m — FFmpeg video exporter for Quartz Composer (Mojave/ARC, 64-bit)
// Inputs:
// Image (image)
// Output Path (string)
// Record (bool toggle; start/stop & finalize)
// Pause (bool toggle; pause encoding, keep file open)
// Duration (sec) (number; 0 = unlimited, counts encoded time only)
// FPS (number; default 30)
// Codec Options (string; e.g. "-c:v libx264 -g 120 -bf 3 -s 1280x720 -preset veryfast -crf 18")
//
// Outputs:
// Is Recording (bool)
// Recorded Seconds (number)
// Recorded Frames (number)
//
// Link with:
// -framework Foundation -framework Quartz -framework OpenGL -framework CoreGraphics
// FFmpeg 4.4.x: avformat,avcodec,avutil,swscale
#import <Quartz/Quartz.h>
#import <CoreGraphics/CoreGraphics.h>
#include <math.h>
#include <string.h>
#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#ifdef __cplusplus
}
#endif
static inline double _clamp(double v,double lo,double hi){ return v<lo?lo:(v>hi?hi:v); }
@interface FFExportPlugIn : QCPlugIn
@property(nonatomic, retain) id<QCPlugInInputImageSource> inputImage;
@property(nonatomic, copy) NSString *inputOutputPath;
@property(nonatomic, assign) BOOL inputRecord;
@property(nonatomic, assign) BOOL inputPause;
@property(nonatomic, assign) double inputDuration;
@property(nonatomic, assign) double inputFPS;
@property(nonatomic, copy) NSString *inputCodecOptions;
@property(nonatomic, assign) double outputIsRecording;
@property(nonatomic, assign) double outputRecordedSeconds;
@property(nonatomic, assign) double outputRecordedFrames;
@end
@implementation FFExportPlugIn
{
// FFmpeg state
AVFormatContext *_fmt;
AVStream *_vstream;
AVCodecContext *_venc;
struct SwsContext *_sws;
AVFrame *_frame;
int _width; // encoded width
int _height; // encoded height
int _srcWidth;
int _srcHeight;
AVRational _timeBase;
double _fps;
int64_t _nextPTS;
int64_t _frameCount;
// QC-time recording state
BOOL _isRecording;
BOOL _prevRecord;
NSTimeInterval _recordStartTime;
NSTimeInterval _lastTime;
double _timeAccum; // seconds accumulated for FPS stepping
double _durationLimit; // seconds (0 = unlimited, based on encoded time)
CGColorSpaceRef _cs;
}
@dynamic inputImage, inputOutputPath, inputRecord, inputPause, inputDuration, inputFPS, inputCodecOptions;
@dynamic outputIsRecording, outputRecordedSeconds, outputRecordedFrames;
+ (NSDictionary*)attributes
{
return @{
QCPlugInAttributeNameKey: @"FFExport (x86_64)",
QCPlugInAttributeDescriptionKey: @"FFmpeg-based video exporter.\nFeed an Image, path, set FPS and toggle Record.\nPause keeps file open but stops encoding frames.\nCodec Options allows ffmpeg-style flags like \"-c:v libx264 -g 120 -bf 3 -s 1280x720\".",
};
}
+ (NSDictionary*)attributesForPropertyPortWithKey:(NSString*)key
{
if ([key isEqualToString:@"inputImage"])
return @{ QCPortAttributeNameKey: @"Image", QCPortAttributeTypeKey: QCPortTypeImage };
if ([key isEqualToString:@"inputOutputPath"])
return @{ QCPortAttributeNameKey: @"Output Path", QCPortAttributeDefaultValueKey: @"" };
if ([key isEqualToString:@"inputRecord"])
return @{ QCPortAttributeNameKey: @"Record", QCPortAttributeTypeKey: QCPortTypeBoolean, QCPortAttributeDefaultValueKey: @0.0 };
if ([key isEqualToString:@"inputPause"])
return @{ QCPortAttributeNameKey: @"Pause", QCPortAttributeTypeKey: QCPortTypeBoolean, QCPortAttributeDefaultValueKey: @0.0 };
if ([key isEqualToString:@"inputDuration"])
return @{ QCPortAttributeNameKey: @"Duration (sec)", QCPortAttributeDefaultValueKey: @0.0 };
if ([key isEqualToString:@"inputFPS"])
return @{ QCPortAttributeNameKey: @"FPS", QCPortAttributeDefaultValueKey: @30.0 };
if ([key isEqualToString:@"inputCodecOptions"])
return @{ QCPortAttributeNameKey: @"Codec Options", QCPortAttributeDefaultValueKey: @"" };
if ([key isEqualToString:@"outputIsRecording"])
return @{ QCPortAttributeNameKey: @"Is Recording" };
if ([key isEqualToString:@"outputRecordedSeconds"])
return @{ QCPortAttributeNameKey: @"Recorded Seconds" };
if ([key isEqualToString:@"outputRecordedFrames"])
return @{ QCPortAttributeNameKey: @"Recorded Frames" };
return nil;
}
+ (NSArray*)sortedPropertyPortKeys
{
return @[
@"inputImage",
@"inputOutputPath",
@"inputRecord",
@"inputPause",
@"inputDuration",
@"inputFPS",
@"inputCodecOptions",
@"outputIsRecording",
@"outputRecordedSeconds",
@"outputRecordedFrames",
];
}
+ (QCPlugInExecutionMode)executionMode { return kQCPlugInExecutionModeProcessor; }
+ (QCPlugInTimeMode) timeMode { return kQCPlugInTimeModeIdle; }
+ (BOOL)allowsSubpatches { return NO; }
// --------------------------------------------------
// Lifecycle
// --------------------------------------------------
- (id)init
{
if ((self = [super init])) {
#ifdef kCGColorSpaceSRGB
_cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
#else
_cs = CGColorSpaceCreateDeviceRGB();
#endif
_fmt = NULL;
_vstream = NULL;
_venc = NULL;
_sws = NULL;
_frame = NULL;
_width = _height = 0;
_srcWidth = _srcHeight = 0;
_fps = 30.0;
_timeBase = (AVRational){1,30};
_nextPTS = 0;
_frameCount = 0;
_isRecording = NO;
_prevRecord = NO;
_recordStartTime = 0.0;
_lastTime = 0.0;
_timeAccum = 0.0;
_durationLimit = 0.0;
}
return self;
}
- (void)dealloc
{
[self _stopEncoding];
if (_cs) { CFRelease(_cs); _cs = NULL; }
}
// --------------------------------------------------
// QC start/stop
// --------------------------------------------------
- (BOOL)startExecution:(id<QCPlugInContext>)context
{
[self setValue:@0.0 forOutputKey:@"outputIsRecording"];
[self setValue:@0.0 forOutputKey:@"outputRecordedSeconds"];
[self setValue:@0.0 forOutputKey:@"outputRecordedFrames"];
return YES;
}
- (void)stopExecution:(id<QCPlugInContext>)context
{
[self _stopEncoding];
}
// --------------------------------------------------
// FFmpeg helpers
// --------------------------------------------------
- (void)_cleanupFFmpeg
{
if (_venc) {
avcodec_free_context(&_venc);
_venc = NULL;
}
if (_fmt) {
if (!(_fmt->oformat->flags & AVFMT_NOFILE) && _fmt->pb) {
avio_closep(&_fmt->pb);
}
avformat_free_context(_fmt);
_fmt = NULL;
}
if (_sws) {
sws_freeContext(_sws);
_sws = NULL;
}
if (_frame) {
av_frame_free(&_frame);
_frame = NULL;
}
_vstream = NULL;
}
// Parse "WxH" resolution string into encW/encH if possible.
static void _parse_resolution(NSString *val, int *encW, int *encH)
{
if (!val || [val length] == 0) return;
NSArray *parts = [val componentsSeparatedByString:@"x"];
if ([parts count] != 2) return;
int w = [parts[0] intValue];
int h = [parts[1] intValue];
if (w > 0 && h > 0) {
*encW = w;
*encH = h;
}
}
// Parse codec options string into:
// - codecName (c:v / codec:v)
// - gop size (g)
// - max B-frames (bf)
// - encode size (s)
// - generic AVDictionary options (preset, tune, crf, profile, etc.)
- (void)_parseCodecOptionsString:(NSString *)opts
codecName:(NSString * __strong *)outCodecName
gopPtr:(int *)outGop
bfPtr:(int *)outBF
encWidth:(int *)outEncW
encHeight:(int *)outEncH
codecOptions:(AVDictionary **)outDict
{
if (outCodecName) *outCodecName = nil;
if (outGop) *outGop = -1;
if (outBF) *outBF = -1;
AVDictionary *d = NULL;
if (!opts || (id)opts == [NSNull null] || [opts length] == 0) {
if (outDict) *outDict = NULL;
return;
}
NSCharacterSet *ws = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSArray<NSString*> *tokens = [opts componentsSeparatedByCharactersInSet:ws];
NSMutableArray<NSString*> *clean = [NSMutableArray arrayWithCapacity:[tokens count]];
for (NSString *t in tokens) {
if ([t length] > 0) [clean addObject:t];
}
for (NSUInteger i = 0; i < [clean count]; ++i) {
NSString *tok = clean[i];
if (![tok hasPrefix:@"-"]) continue;
NSString *key = [tok substringFromIndex:1];
NSString *val = (i + 1 < [clean count]) ? clean[i+1] : nil;
// Normalize :v suffix (foo:v -> foo) for generic options
NSString *plainKey = key;
if ([plainKey hasSuffix:@":v"]) {
plainKey = [plainKey substringToIndex:plainKey.length - 2];
}
// Special cases
if (([key isEqualToString:@"c:v"] || [key isEqualToString:@"codec:v"]) && val) {
if (outCodecName) *outCodecName = val;
i++; // consume value
continue;
}
if ([plainKey isEqualToString:@"g"] && val && outGop) {
*outGop = [val intValue];
i++;
continue;
}
if ([plainKey isEqualToString:@"bf"] && val && outBF) {
*outBF = [val intValue];
i++;
continue;
}
if ([plainKey isEqualToString:@"s"] && val && outEncW && outEncH) {
_parse_resolution(val, outEncW, outEncH);
i++;
continue;
}
// Generic codec options -> AVDictionary
if (val) {
av_dict_set(&d, [plainKey UTF8String], [val UTF8String], 0);
i++;
}
}
if (outDict) *outDict = d;
}
// srcW/srcH = QC image size; -s may override encode size.
- (BOOL)_startEncodingWithSourceWidth:(int)srcW
sourceHeight:(int)srcH
fps:(double)fps
path:(NSString *)path
options:(NSString *)optString
{
[self _cleanupFFmpeg];
if (srcW <= 0 || srcH <= 0) return NO;
if (fps <= 0.0) fps = 30.0;
_srcWidth = srcW;
_srcHeight = srcH;
_fps = fps;
int encW = srcW;
int encH = srcH;
NSString *codecName = nil;
int gopSize = -1;
int maxBF = -1;
AVDictionary *codecOpts = NULL;
[self _parseCodecOptionsString:optString
codecName:&codecName
gopPtr:&gopSize
bfPtr:&maxBF
encWidth:&encW
encHeight:&encH
codecOptions:&codecOpts];
if (encW <= 0 || encH <= 0) {
encW = srcW;
encH = srcH;
}
_width = encW;
_height = encH;
int fpsInt = (int)llround(fps);
if (fpsInt < 1) fpsInt = 1;
_timeBase = (AVRational){1, fpsInt};
_nextPTS = 0;
_frameCount = 0;
// Resolve file:// if present
NSString *realPath = path;
if ([realPath hasPrefix:@"file://"]) {
realPath = [[NSURL URLWithString:realPath] path];
}
const char *filename = [realPath fileSystemRepresentation];
AVOutputFormat *ofmt = NULL;
avformat_alloc_output_context2(&_fmt, NULL, NULL, filename);
if (!_fmt) {
avformat_alloc_output_context2(&_fmt, NULL, "mp4", filename);
}
if (!_fmt) {
if (codecOpts) av_dict_free(&codecOpts);
return NO;
}
ofmt = _fmt->oformat;
// Choose codec: from c:v if provided, otherwise H.264
const AVCodec *codec = NULL;
if (codecName && [codecName length] > 0) {
codec = avcodec_find_encoder_by_name([codecName UTF8String]);
}
if (!codec) {
codec = avcodec_find_encoder(AV_CODEC_ID_H264);
}
if (!codec) {
if (codecOpts) av_dict_free(&codecOpts);
return NO;
}
_vstream = avformat_new_stream(_fmt, codec);
if (!_vstream) {
if (codecOpts) av_dict_free(&codecOpts);
return NO;
}
_vstream->id = _fmt->nb_streams - 1;
_venc = avcodec_alloc_context3(codec);
if (!_venc) {
if (codecOpts) av_dict_free(&codecOpts);
return NO;
}
_venc->codec_id = codec->id;
_venc->width = encW;
_venc->height = encH;
_venc->pix_fmt = AV_PIX_FMT_YUV420P;
_venc->time_base = _timeBase;
_vstream->time_base = _timeBase;
_venc->framerate = (AVRational){ fpsInt, 1 };
_venc->gop_size = (gopSize > 0 ? gopSize : fpsInt);
_venc->max_b_frames = (maxBF >= 0 ? maxBF : 2);
_venc->bit_rate = 8 * 1000 * 1000; // default 8 Mbps; can be overridden via codecOptions if codec supports it
if (ofmt->flags & AVFMT_GLOBALHEADER) {
_venc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// Only set default preset/tune if user didn't override them.
if (_venc->priv_data) {
if (!av_dict_get(codecOpts, "preset", NULL, 0)) {
av_dict_set(&codecOpts, "preset", "medium", 0);
}
if (!av_dict_get(codecOpts, "tune", NULL, 0)) {
av_dict_set(&codecOpts, "tune", "animation", 0);
}
}
if (avcodec_open2(_venc, codec, &codecOpts) < 0) {
if (codecOpts) av_dict_free(&codecOpts); // any leftovers
return NO;
}
if (codecOpts) av_dict_free(&codecOpts);
if (avcodec_parameters_from_context(_vstream->codecpar, _venc) < 0) {
return NO;
}
if (!(ofmt->flags & AVFMT_NOFILE)) {
if (avio_open(&_fmt->pb, filename, AVIO_FLAG_WRITE) < 0) {
return NO;
}
}
if (avformat_write_header(_fmt, NULL) < 0) {
return NO;
}
_frame = av_frame_alloc();
if (!_frame) return NO;
_frame->format = _venc->pix_fmt;
_frame->width = _venc->width;
_frame->height = _venc->height;
if (av_frame_get_buffer(_frame, 32) < 0) {
return NO;
}
// Swscale from QC's BGRA source size -> encoded size
_sws = sws_getContext(_srcWidth, _srcHeight, AV_PIX_FMT_BGRA,
encW, encH, _venc->pix_fmt,
SWS_BICUBIC, NULL, NULL, NULL);
if (!_sws) return NO;
return YES;
}
- (BOOL)_encodeFrameWithBGRA:(const uint8_t *)src
rowBytes:(int)rowBytes
{
if (!_fmt || !_venc || !_frame || !_sws) return NO;
if (av_frame_make_writable(_frame) < 0) return NO;
const uint8_t *srcSlice[4] = { src, NULL, NULL, NULL };
int srcStride[4] = { rowBytes, 0, 0, 0 };
sws_scale(_sws, srcSlice, srcStride, 0, _srcHeight,
_frame->data, _frame->linesize);
_frame->pts = _nextPTS++;
int ret = avcodec_send_frame(_venc, _frame);
if (ret < 0) return NO;
AVPacket *pkt = av_packet_alloc();
if (!pkt) return NO;
for (;;) {
ret = avcodec_receive_packet(_venc, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
av_packet_free(&pkt);
return NO;
}
pkt->stream_index = _vstream->index;
av_packet_rescale_ts(pkt, _venc->time_base, _vstream->time_base);
ret = av_interleaved_write_frame(_fmt, pkt);
av_packet_unref(pkt);
if (ret < 0) {
av_packet_free(&pkt);
return NO;
}
}
av_packet_free(&pkt);
_frameCount++;
return YES;
}
- (void)_flushEncoder
{
if (!_fmt || !_venc) return;
int ret = avcodec_send_frame(_venc, NULL); // flush
if (ret < 0) {
return;
}
AVPacket *pkt = av_packet_alloc();
if (!pkt) return;
for (;;) {
ret = avcodec_receive_packet(_venc, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) break;
pkt->stream_index = _vstream->index;
av_packet_rescale_ts(pkt, _venc->time_base, _vstream->time_base);
av_interleaved_write_frame(_fmt, pkt);
av_packet_unref(pkt);
}
av_packet_free(&pkt);
}
- (void)_stopEncoding
{
if (_isRecording) {
[self _flushEncoder];
if (_fmt) {
av_write_trailer(_fmt);
}
}
[self _cleanupFFmpeg];
_isRecording = NO;
}
// --------------------------------------------------
// Execute
// --------------------------------------------------
- (BOOL)execute:(id<QCPlugInContext>)context
atTime:(NSTimeInterval)time
withArguments:(NSDictionary*)arguments
{
@autoreleasepool {
id<QCPlugInInputImageSource> imgSrc = [self valueForInputKey:@"inputImage"];
NSString *path = [self valueForInputKey:@"inputOutputPath"];
if (!path || (id)path == [NSNull null]) path = @"";
double fpsVal = [[self valueForInputKey:@"inputFPS"] doubleValue];
if (fpsVal <= 0.0) fpsVal = 30.0;
fpsVal = _clamp(fpsVal, 1.0, 240.0);
double durVal = [[self valueForInputKey:@"inputDuration"] doubleValue];
if (durVal < 0.0) durVal = 0.0;
NSString *codecOpts = [self valueForInputKey:@"inputCodecOptions"];
if (!codecOpts || (id)codecOpts == [NSNull null]) codecOpts = @"";
BOOL recVal = ([[self valueForInputKey:@"inputRecord"] doubleValue] >= 0.5);
BOOL pauseVal = ([[self valueForInputKey:@"inputPause"] doubleValue] >= 0.5);
BOOL recEdgeOn = (recVal && !_prevRecord);
BOOL recEdgeOff = (!recVal && _prevRecord);
_prevRecord = recVal;
// Start recording on Record rising edge
if (recEdgeOn && !_isRecording && imgSrc && path.length > 0) {
CGRect bounds = [imgSrc imageBounds];
int w = (int)CGRectGetWidth(bounds);
int h = (int)CGRectGetHeight(bounds);
if (w > 0 && h > 0) {
if ([self _startEncodingWithSourceWidth:w
sourceHeight:h
fps:fpsVal
path:path
options:codecOpts]) {
_isRecording = YES;
_durationLimit = durVal;
_recordStartTime = time;
_lastTime = time;
_timeAccum = 0.0;
}
}
}
// Recorded seconds based on encoded frames (pause-safe)
double recordedSecs = 0.0;
if (_fps > 0.0) recordedSecs = (double)_frameCount / _fps;
// Duration auto-stop based on encoded length only
if (_isRecording && _durationLimit > 0.0) {
if (recordedSecs >= _durationLimit) {
[self _stopEncoding];
}
}
// Stop & finalize when Record is untoggled
if (_isRecording && recEdgeOff) {
[self _stopEncoding];
}
// Encode frames while recording and NOT paused
if (_isRecording && imgSrc) {
double dt = time - _lastTime;
if (dt < 0.0) dt = 0.0;
_lastTime = time;
if (!pauseVal) {
_timeAccum += dt;
double frameInterval = 1.0 / _fps;
CGRect bounds = [imgSrc imageBounds];
if ([imgSrc lockBufferRepresentationWithPixelFormat:QCPlugInPixelFormatBGRA8
colorSpace:_cs
forBounds:bounds]) {
const void *base = [imgSrc bufferBaseAddress];
size_t rowBytes = [imgSrc bufferBytesPerRow];
while (_timeAccum >= frameInterval) {
[self _encodeFrameWithBGRA:(const uint8_t *)base
rowBytes:(int)rowBytes];
_timeAccum -= frameInterval;
}
[imgSrc unlockBufferRepresentation];
}
if (_fps > 0.0) recordedSecs = (double)_frameCount / _fps;
}
}
[self setValue:@(_isRecording ? 1.0 : 0.0) forOutputKey:@"outputIsRecording"];
[self setValue:@(recordedSecs) forOutputKey:@"outputRecordedSeconds"];
[self setValue:@((double)_frameCount) forOutputKey:@"outputRecordedFrames"];
return YES;
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment