Sunday, July 15, 2007

Ahhh - CIFilter ... bliss

So, after ranting and raving about QT based effects I decided to chuck them and go for CoreVideo. After getting the movie export working using a procedure based approach (so that I can mess with the frames before they are compressed) I turned to CIFilter.


Oh what joy. Ahhhh. Awesome.


So instead of having to write a billion lines of code to find out what effects exist, you can just do this:


NSArray *filters = [CIFilter filterNamesInCategories:[NSArray arrayWithObject:kCICategoryBlur]];


Which will get you all the blur filters. The really nice thing about CIFilter is the metadata. It makes it reasonably simple to create a user interface on the fly, given any CIFilter attributes. I won't go over the whole thing here as it's well documented elsewhere suffice too say that you've got all the info you need to build a UI dynamically.


When I was still considering the use of QTEffects for Squish I had originally thought that I might hard code a few commonly used filter interfaces such as Gamma, Brightness/Contrast/Saturation, and so on. The primary reason was that Squish isn't a video editor. It doesn't handle transitions and it's not intended to be able to insert complex filter overlays onto video - the type of thing that iMovie/FCP are good at. But when I saw the list of filters, I thought "hmm. that's quite a few." and realized I was going to have to be a bit smarter.


So Squish now builds it's filter inspector based on the properties of the filter you're looking at. One part I really liked, which is a mix of standard NSObject and the CIFilter interface was the elegant way by which all filter attributes can be extracted/and or set on the filter. In three words: Key Value Coding.


For example - lets say you want to get a mutable dictionary of all filter attributes that matter to you, change some values and the set the filter values again so that you can observe the result. Step one - get the attributes that matter:


CIFilter *filter = [CIFilter filterWithName:effectName];
[filter setDefaults];
NSDictionary *inputKeyDictionary = [filter dictionaryWithValuesForKeys:[filter inputKeys]];
NSMutableDictionary *attrs = [NSMutableDictionary dictionaryWithDictionary:inputKeyDictionary];

Er, well - that was simple :-) Not only are they easy to get but they will have sensible defaults due to the setDefaults message. So what if you've now modified the attributes and want to set them back? Again, so so trivial since the CIFilter simply uses the existing KVC methods and we already have all of the input attributes. We can set them back on the filter like so:

CIFilter *aFilter = [CIFilter filterWithName:effectName];
[aFilter setValuesForKeysWithDictionary: attrs];

Anyone seasoned in Cocoa will see the above as trivial. It is, certainly. But it's such a joy to use having just jumped off the run away train that is QT Atoms that I feel the need to write something. It's also nice to praise a sensible elegant API when you see it.


What this all means in reality is that I've been able to construct a full working CoreImage effects based filtering mechanism in about 3 days total. That's pretty impressive if you ask me (Ok, yes, it'd be nice if there were a standard inspector available, but I'll live) - and getting it working with the visual context of QT was a breeze.


Having wrapped up the filter code, it was then possible to apply a filter to a frame by simply calling one helper method (whose implementation isn't that complex). In fact; there's more code to get the bitmap data out (so that I can hand it to QT) that there is to perform the filtering!

CVPixelBufferRef ref = [currentMovieFrame pixelBufferRef];
CVReturn lockResult = CVPixelBufferLockBaseAddress(ref, 0);
if(lockResult == kCVReturnSuccess) {
void *dataPtr = CVPixelBufferGetBaseAddress(ref);
if([parameters effectStack]) {
CIImage *image = [[CIImage imageWithCVImageBuffer:ref] autorelease];
CIFilter *filter = [CIFilterFactory generateFromEffectStack:image effectStack:[parameters effectStack]];
if(filter) {
CIImage *outImage = [filter valueForKey:@"outputImage"];
int width = CVPixelBufferGetWidth(ref);
int height = CVPixelBufferGetHeight(ref);
int bytesPerRow = CVPixelBufferGetBytesPerRow(ref);
CGColorSpaceRef cSpace = CGColorSpaceCreateDeviceRGB();

CGContextRef bitmapCtx = CGBitmapContextCreate(dataPtr, width, height, 8, bytesPerRow, cSpace, kCGImageAlphaPremultipliedFirst);
CIContext *ciContext = [CIContext contextWithCGContext:bitmapCtx options:nil];
CGImageRef cgImage = [ciContext createCGImage:outImage fromRect:[outImage extent]];
CGContextDrawImage(bitmapCtx, [outImage extent], cgImage);

CGContextRelease(bitmapCtx);
CGImageRelease(cgImage);
CGColorSpaceRelease(cSpace);
}
}
long dataSize = CVPixelBufferGetDataSize(ref);
(*outputImageDescription)->dataSize = dataSize;
(*outputImageDescription)->depth = 32;
CVPixelBufferUnlockBaseAddress(ref, 0);

// etc etc - all QT based stuff from here on


Before you scream - Squish isn't going to use the method above - it's quite ineffient (it creates a bitmap and throws it away, for every video frame). However; it's simple to read - and good for demonstrating the technique.

CIFilter *filter = [CIFilterFactory generateFromEffectStack:image effectStack:[parameters effectStack]];

The above gets us a fully initialized filter chain (In Squish, filters can be layered and thus are chained together). We simply get the output image and go through the steps above to extract some bitmap data for use with QT. Pretty impressive if you ask me.


In closing - it's been a week of productivity because an API exists that a) has been thought out, b) integrates well with CoreGraphics and c) "just works". It's really nice when this happens!

Monday, July 9, 2007

Quicktime Atoms

Rant mode: Disabled
Here we are. Slighly cold day, working away on Squish in some attempt to extract various bits of information about the selected compression fromat, its frame rate, audio properties etc etc. I want to do this so I can show users an "executive summary" of what their compression preset is all about - without them having to enter the QT settings dialogs to find out. Of course all these things are stored in something called a QTAtomContainer.

OK. I can handle that. How hard can it be?

... a few hours pass ...

Rant mode: Enabled

I have now discovered the source of all woe in the world. QuickTime Atom Containers, Atoms, Atom IDs, Atom anythings. grrr. I can understand that this is a legacy carry over, from a time perhaps when space efficiency was important - but I can't think of anything more painful right now than using these things... aside from perhaps trying to cut my leg off using the back edge of a rusty razor blade.

Don't get me wrong here. I like QuickTime (the new parts). Rather, I like what QuickTime can DO for me. Lots. It's pretty cool. But these 1980's C API's are doing my HEAD IN. Every time I come to examine these APIs and use them, I cringe.... lets demonstrate the joy:

So - I want to get some details about a QTEffect.
Lets say I want to get the name of said effect - and that I've already gone through a bunch of pain to get the effect description QTAtomContainer. I can get the name by simply doing:

  
QTAtom effectNameAtom = QTFindChildByIndex(container, kParentAtomIsContainer, kEffectNameAtom, effectIndex, nil);
long dataSize;
Ptr atomData;
OSErr err = QTGetAtomDataPtr(container, effectNameAtom, &dataSize, &atomData);
if(err != noErr) {
[NSException raise:@"Effects" format:@"Can't get name of the effect"];
} else {
[self setName:[NSString stringWithCString:atomData length:dataSize]];
}


Easy huh? OK. Lets say I wanted to create an effect - and apply that effect to a video. I'll make it simple too - lets say that effect is to operate on just the existing video - it's not a transition. Righteo then. Lets begin.

Well, we need to setup a effect description ... simple enough:
 
QTAtomContainer effectsDescription;
QTNewAtomContainer(&effectsDescription);
QTInsertChild(effectsDescription,
kParentAtomIsContainer,
kParameterWhatName,
kParameterWhatID,
0,
sizeof(effectCode),
&effectCode,
nil);
// EndianU32_NtoB(3);
long value = 3;

QTInsertChild(effectsDescription,
kParentAtomIsContainer,
'ksiz',
1,
0,
sizeof(value),
&value,
nil);

if(effectCode == kEdgeDetectImageFilterType) {
value = 1;
QTInsertChild(effectsDescription,
kParentAtomIsContainer,
'colz',
1,
0,
sizeof(value),
&value,
nil);
}
OSType myType = [self sourceName];
QTInsertChild(effectsDescription, kParentAtomIsContainer, kEffectSourceName, 1, 0, sizeof(myType), &myType, NULL);


Er, yay. But wait - that's not all! We need also to add this as a sample to a new track. This is how we tell QT that's it can apply this effect.

 
Movie sourceMovie = GetTrackMovie(sourceTrack);

// Now add an effect
NSRect trackSize = [self trackRect:sourceTrack];
theEffectsTrack = NewMovieTrack(sourceMovie, IntToFixed(trackSize.size.width), IntToFixed(trackSize.size.height), 0);
if(theEffectsTrack == nil) {
[self log:@" ** Unable to create the effects track"];
return;
}

[self log:@"Created a new effects track with %@", NSStringFromSize(trackSize.size)];
theEffectsMedia = NewTrackMedia(theEffectsTrack, VideoMediaType, GetMovieTimeScale(sourceMovie), nil, 0);
if(theEffectsMedia == nil) {
[self log:@"Unable to add the new media to the video, for the effect track"];
} else {
[self log:@"Added effects media, with duration %lld", GetMovieDuration(sourceMovie)];
}

ImageDescriptionHandle sampleDescription = [self createEffectImageDescription:effectCode size:trackSize.size];
BeginMediaEdits(theEffectsMedia);
// Add the sample to the media
TimeValue sampleTime = 0;
OSStatus err = noErr;
BAILSETERR(AddMediaSample(theEffectsMedia,
(Handle) effectsDescription,
0,
GetHandleSize((Handle) effectsDescription),
GetMovieDuration(sourceMovie),
(SampleDescriptionHandle) sampleDescription,
1,
0,
&sampleTime));
// End the media editing session
EndMediaEdits(theEffectsMedia);
BAILSETERR(InsertMediaIntoTrack(theEffectsTrack, 0, sampleTime, GetMediaDuration(theEffectsMedia), fixed1));

...

- (ImageDescriptionHandle) createEffectImageDescription:(OSType)effectCode size:(NSSize)effectSize {
ImageDescriptionHandle sampleDescription = nil;
[self log:@"Creating sample description for effect %@", [NSString osTypeToString:effectCode]];
MakeImageDescriptionForEffect(effectCode, &sampleDescription);
(**sampleDescription).vendor = kAppleManufacturer;
(**sampleDescription).temporalQuality = codecNormalQuality;
(**sampleDescription).spatialQuality = codecNormalQuality;
(**sampleDescription).width = effectSize.width;
(**sampleDescription).height = effectSize.height;
return sampleDescription;
}



Whew. But guess what? YES! There's MORE! It's all well and good to describe an effect, but we also need to tell QT about how to link it all together. Should be simple enough:

 
OSErr err = noErr;
QTNewAtomContainer(&inputMap);

long refIndex;
QTAtom inputAtom;

// Add a reference to the video track
BAILSETERR(AddTrackReference(theEffectsTrack, sourceTrack, kTrackModifierReference, &refIndex));

// Add a reference into the input map
QTInsertChild(inputMap, kParentAtomIsContainer, kTrackModifierInput, refIndex, 0, 0, nil, &inputAtom);

OSType type = [self safeOStype:kTrackModifierTypeImage];
QTInsertChild(inputMap, inputAtom, kTrackModifierType, 1, 0, sizeof(type), &type, nil);

type = [self sourceName];
QTInsertChild(inputMap, inputAtom, kEffectDataSourceType, 1, 0, sizeof(type), &type, nil);

BAILSETERR(SetMediaInputMap(theEffectsMedia, inputMap));


Er, wow.
Now it's slightly unfair of me to complain here. A couple of things are going on that simple CoreImage Filtering can't match.

1) It's applying the filter to a video - not just a frame.
2) The API is allowing us to choose the source video track, so effects can be layered "easily".

However. It doesn't excuse the verbosity of this entire API/method. Every time I come to use this tech I'm absolutely Stunned (yes, stunned with a capital S) at how much work I have to do. Why couldn't we have an API like so? (OK, I'm intentionally ignoring everyone who doesn't use Objective-C here)...


QTEffect *effect = [[QTEffect alloc] initWithName:kQTBlurEffect];
NSArray *defaultParameters = [effect defaults];

// ... go off and modify these - whatever.
// In my example above they are hard coded).

[effect setParameters:newParameters];
[effect applyTo:myQtMovie];


I guess my rant is really that there appears to be no other way to do this "simple" task. Simple? OK, it's not simple. Application of video effects must be reasonably tricky. But it's now "simple" compared to other APIs available to us right now. CoreImage Filtering is a great example. It provides the paramter metadata and defaults as easily understood NSDictionary instances. Bam. Done. I spent a considerable time trying to extract the presets from a QTEffect but with no joy. I know the stuff is there, but it's soooo cryptic. Ghaaa.

I can only assume that QTEffects is dead. It's certainly not been kept up to date (even though underneath it I think has been udpated) from an API point of view. In fact for Squish I've taken two steps back now. Why would I use QTEffects (even though they do what I want) when CoreImage seems the future? Yes, I'm going to have to rewrite my recompression engine (because the change away from the method I'm using now so that I can use CoreImage is actually quite major. QT has a very cool function ConvertMovieToDataRef that can magically (given the right input) transcode one movie to another) - but I can't see why I would want to stay with API's like QTAtom and QTEffects. The pain is too great.

I guess I'd hoped that QTEffect would be a neat way for me to have effects applied to the video without having to unpack each source video frame, perform the effect myself and then recompress the video myself. It seems it is not to be. Ah well, I guess you don't get the best of both worlds (the cools stuff of QT, with the cool stuff of Core*) without doing some work.

Roll on Leopard.

Thursday, June 28, 2007

It's done

So, version 1.41 rolls out the door. I've been working flat out on it since I found out about the recording bugs and; of course; activation.

Well - some will be pleased to know it's gone. Replaced with a typical serial number system. What else? A new feature! Amazing - feels like I've not done "cool new feature work" for a little while now (er, that'll be because I've not - I've spent the last two months working on the website and Squish).

Anyway - done! Enjoy!

Friday, June 22, 2007

Squish!

Some of you many know that SWB is working on another product. Some of you may even have seen it and tried it. It's all true (which is a good thing).

It became clear that H264 is excellent for compression but not such a hot choice if you either don't have a grunty machine (think: G4 and some G5's) or are capturing the entire desktop ... oh; and of course forget getting a high frame rate.

For all my screencasts, I use either Apple Intermediate or Apple Animation. Good frame rate, low CPU usage. Huuuuuuge files. I've got QT Pro. I think loads of people have. But it's interface is ... shall we say ... ik?

That was my motivator. I'm going to release a free time limited feedback demo (just like I did with iShowU) when I've got the essential 1.0 features in place. There's quite a bit that won't be in the 1.0 that I'd like to do, but if I do all those then it'll never get released. And that would suck.

The activation system

Busy times, busy times. The new system is live. Pretty obvious I guess if you've downloaded anything from the 1.35 release onward.

I must admit; I didn't expect as much "resistance" to the new system as I've had. That's not to say hundreds of people have emailed me negative things - but certainly a handful have. On the other hand; I've had nearly an equal number of people say it's cool.

I didn't intend to: But I think this might have turned into a bit of a rant. Of course; I don't intend to offend anyone - but if you'd like to get a small glimpse into my last four days read on. If you don't care about the "why" of activation etc then just skip this post entirely.

The Defense: If you're wondering "why? why?" - there are very good reasons:

* Support - If I can cut down on the number of emails relating to "I can't type my code in" then I can work on more products, or fix issues, or do other things. Those registration issues were only going to get worse as the sales increased. This is the primary reason for activation. I'm not jumping on a bandwagon of trying to be mean to users or anything like that. Seriously - the whole point is that it's supposed to be easier for you to activate. If just have to use one piece of information (i.e: the same username and password as you did when you purchased) then there's less chance of something going wrong. More chance that it'll "just work" and you'll be using the software as you want to.

* Profit - Simply put - the business can make more money if it goes through paypal. And that in the end benefits the customer because shinywhitebox will still be around. I suppose some people might see this as a greedy-money-grabbing-not-caring-about-the-customer exercise. I'm not going to get into arguments about it though. The business reasons are above - I can't force you to have my opinion.

* The next product - In reality Kagi can be a bit of a handful to setup. I'm not slagging them here OK? I've dealt with them a reasonable amount and they have excellent support. But their website and product interface leaves lots to be desired. I'd rather not have to go through that for the next product. Creating this framework will mean that the next product (in the wings) can be bought to market earlier than it would have been otherwise.

Teething - It's fair to say there were some teething issues. When it was first released; it locked the activation to a single machine. Thus it appeared that something the user had (the ability to install one license on any number of machines) was being removed. So we went for a balance. Let you activate a personal license 5 times so that you don't keep on having to deactivate/activate on other machines if you want to use it. We figure that's being quite generous.

The Machine Serial Number - This caused a bit of (and probably still does) a stir. Some people are unhappy that this is used in the activation process. As I mentioned on the website - we have to use something in order to identify the machine (and a number of other well known authors also use it - you just might not realize it, is all). That's all it's for. Identifying that yes, this machine has been activated. If we use some other piece of information then it can change more easily (at which time your activation becomes invalid). All I can say (and again, if you choose not to believe this there is little I can do) is that were not doing anything other than what I've described here with that number. No one else can see it. No one else can access it. It's used for activation only.

I'll stop there before I really start ranting! No, overall it's been a success. I don't believe that most people who find something works actually email the author to let them know. It's a human thing right? We just expect it. So given that there have been just a handful of negative emails, I'd say activation was a good release (if unexpected by most people).

Tuesday, March 27, 2007

Recent optimizations

It's been interesting lately - I've been delving into OpenGL more, in an effort to optimize iShowU. Read more here http://forums.shinywhitebox.com/viewtopic.php?t=438.

It all started when I wanted to create some videos showing the best settings for recording games (World of Warcraft being my main "waste of time" :-). I thought to myself ... "I wonder just how inefficient I'm being here". I set to work with the OpenGLProfiler, and found that of all iShowUs' time, 22-45% was spent in OpenGL (30-45% if capturing from two screens at the same time) while recording World of Warcraft.

I ended up pulling lots of capture code apart, and in the end found the problem (many thanks to the OpenGL list hosted by Apple). BTW - if you ever see the apple code example that implies that a glTexImage2D buffer can be different from a glGetTexImage buffer (i.e: they point to two different memory locations) - it's not true. Well, not true in my case anyway.

The omtimizations, from what I've observed, have been excellent.

It's not going to change the world, but on PPC capturing WoW at 1056x900 went from 17fps to 24fps. Even better gains on Intel. Full screen capture on an Intel MBP (15") went from 18fps average, to 25fps with some CPU time to spare. So in short, iShowU is able to pull data from the OpenGL subsystem much more efficiently. The downside is that this can have a negative performance imapact! If more frames can be captured, then they need to be compressed and stored. This means that the realtime compressor is working harder, and the harddisk bandwidth can be maxed out earlier.

In some ways then, iShowU will now let you "get it into trouble" a little easier. You can bump up the frame rates with Apple Animation for example, and watch the automatic capture status pop into view (after a view seconds) telling you that the system is generating more frames than can be written to disk (note that this is easy on a laptop, because the disk is typically a bit slower than a desktop machine).

But overall it's all good. In all capture cases, performance is improved. That was the goal :-)