iTunes Scripting Bridge reveal does not work - cocoa

The following code should show a certain track in iTunes:
NSString* iTunesPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:#"com.apple.iTunes"];
iTunesApplication *iTunes = nil;
if ( iTunesPath ) {
iTunes = [[SBApplication alloc] initWithURL:[NSURL fileURLWithPath:iTunesPath]];
[iTunes setDelegate:self];
}
iTunesSource *librarySource = nil;
NSArray *sources = [iTunes sources];
for (iTunesSource *source in sources) {
if ([source kind] == iTunesESrcLibrary) {
librarySource = source;
break;
}
}
SBElementArray *libraryPlaylist = [librarySource libraryPlaylists];
iTunesLibraryPlaylist *iTLibPlaylist = nil;
if ([libraryPlaylist count] > 0) {
iTLibPlaylist = [libraryPlaylist objectAtIndex:0];
}
SBElementArray *fileTracks = [iTLibPlaylist fileTracks];
iTunesFileTrack *track = [fileTracks objectAtIndex:4];
NSLog(#"Try to reveal track: %# at path :%#",[track description],[[track location] path]);
[track reveal];
Output:
Try to reveal track: <ITunesFileTrack #0x1364ed20: ITunesFileTrack 4 of ITunesLibraryPlaylist 0 of ITunesSource 0 of application "iTunes" (2474)> at path :/Users/...
But absolutely noting happens. What am I doing wrong?
(iTunes Version: 9.0.3)

The Library playlist doesn't exist anymore in the UI; it's there in the model, so it shows up in AppleScript, but trying to reveal it or anything in it won't do anything in the UI, as you saw. You can reproduce this in AppleScript as well (reveal track 5 of library playlist 1 of source 1).
The solution is to talk to the Music playlist, not the Library playlist. “Music” is the second playlist—playlist 2 in AppleScript, [[librarySource playlists] objectAtIndex:1] in Cocoa.
If you want to reveal a playing item in whatever playlist it's playing in, use reveal current track (which should be [[iTunes currentTrack] reveal], although I haven't tested that).

This helped me solve a related issue. Thanks.
I would recommend against using [[librarySource playlists] objectAtIndex:1] to get the playlist. It feels too much like guessing. You should also avoid iterating through the arrays from Scripting Bridge with a for loop.
This example solves both problems:
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
iTunesSource *library = [[[[iTunes sources] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %i", iTunesESrcLibrary]] objectAtIndex:0];
iTunesLibraryPlaylist *lp = [[[[library playlists] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"specialKind == %i", iTunesESpKMusic]] objectAtIndex:0];
[[library playlists] objectWithName:#"Music"] also works, but I’m not sure if that’s locale dependent (and the name could change in a future update).

Just to add to the answer of Rob McBroom, it would be even better to use firstObject instead of objectAtIndex:0. It will prevent an exception in case your query fails and returns an empty array. This will happen when you search for the Internet radio source (kind == iTunesESrcRadioTuner) and the Internet Radio library is disabled in the preferences.
iTunesApplication* iTunesApp = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
iTunesSource* radioTunerSource = [[[[iTunesApp sources] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %i", iTunesESrcRadioTuner]] firstObject];

Related

Looping Through NSArray

This is suppose to play all the songs in the NSArray but it only plays one and stops. What am I doing wrong? The array contains a whole bunch of links to different .mp3 formatted songs.
NSArray *array = [myString componentsSeparatedByString:#"\n"];
int nextTag = 1;
for (soundPath in array) {
NSURL *url = [NSURL URLWithString:soundPath];
asset = [AVURLAsset URLAssetWithURL:url options:nil];
yourMediaPlayer = [[MPMoviePlayerController alloc] initWithContentURL:url];
yourMediaPlayer.controlStyle = MPMovieControlStyleNone;
yourMediaPlayer.currentPlaybackTime = 0;
yourMediaPlayer.shouldAutoplay = FALSE;
[yourMediaPlayer play];
nextTag++;
}
Instead of trying to get MPMoviePlayerController play a list of items use AVQueuePlayer instead. It has a convenient advanceToNextItem method.
You can create a player already loaded with items (class AVPlayerItem) either with initWithItems or queuePlayerWithItems. Then just call play on it (inherited from AVPlayer) and it should play the items one after the other.
See:
AVQueuePlayer docs
AVPlayer docs
AV Foundation Programming Guide
AVPlayerItem docs

AvMutableComposition issues having black frame at the end

I capturing a video using AVCaptureConnection in my iOS app. After that I add some images in the video as CALayers. Everything is working fine but I get a black frame at the very end of the resulting video after adding images. There is no frame of actual audio/video that has been affected in this. For audio I am extracting it and changing its pitch and then add it using AVMutableComposition. Here is the code that I am using. Please help me with what I am doing wrong or do I need to add something else.
cmp = [AVMutableComposition composition];
AVMutableCompositionTrack *videoComposition = [cmp addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *audioComposition = [cmp addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
AVAssetTrack *sourceVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
AVAssetTrack *sourceAudioTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
[videoComposition insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:sourceVideoTrack atTime:kCMTimeZero error:nil] ;
[audioComposition insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:sourceAudioTrack atTime:kCMTimeZero error:nil];
animComp = [AVMutableVideoComposition videoComposition];
animComp.renderSize = CGSizeMake(320, 320);
animComp.frameDuration = CMTimeMake(1,30);
animComp.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];
// to gather the audio part of the video
NSArray *tracksToDuck = [cmp tracksWithMediaType:AVMediaTypeAudio];
NSMutableArray *trackMixArray = [NSMutableArray array];
for (NSInteger i = 0; i < [tracksToDuck count]; i++) {
AVMutableAudioMixInputParameters *trackMix = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:[tracksToDuck objectAtIndex:i]];
[trackMix setVolume:5 atTime:kCMTimeZero];
[trackMixArray addObject:trackMix];
}
audioMix = [AVMutableAudioMix audioMix];
audioMix.inputParameters = trackMixArray;
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
instruction.timeRange = CMTimeRangeMake(kCMTimeZero, [asset duration]);
AVMutableVideoCompositionLayerInstruction *layerVideoInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoComposition];
[layerVideoInstruction setOpacity:1.0 atTime:kCMTimeZero];
instruction.layerInstructions = [NSArray arrayWithObject:layerVideoInstruction] ;
animComp.instructions = [NSArray arrayWithObject:instruction];
[self exportMovie:self];
This is my method for exporting the video
-(IBAction) exportMovie:(id)sender{
//successCheck = NO;
NSArray *docPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *tempPath = [docPaths objectAtIndex:0];
//NSLog(#"Temp Path: %#",tempPath);
NSString *fileName = [NSString stringWithFormat:#"%#/Final.MP4",tempPath];
NSFileManager *fileManager = [NSFileManager defaultManager] ;
if([fileManager fileExistsAtPath:fileName ]){
NSError *ferror = nil ;
[fileManager removeItemAtPath:fileName error:&ferror];
}
NSURL *exportURL = [NSURL fileURLWithPath:fileName];
AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:cmp presetName:AVAssetExportPresetMediumQuality] ;
exporter.outputURL = exportURL;
exporter.videoComposition = animComp;
//exporter.audioMix = audioMix;
exporter.outputFileType = AVFileTypeQuickTimeMovie;
[exporter exportAsynchronouslyWithCompletionHandler:^(void){
switch (exporter.status) {
case AVAssetExportSessionStatusFailed:{
NSLog(#"Fail");
break;
}
case AVAssetExportSessionStatusCompleted:{
NSLog(#"Success video");
});
break;
}
default:
break;
}
}];
NSLog(#"outside");
}
there is a property of exportsession to give the time range ,
try giving time range little less than the actual time (few nano seconds less)
You can get the true video duration from AVAssetTrack. The duration of AVAsset is sometimes longer than AVAssetTrack' one.
Check durations out like this.
print(asset.duration.seconds.description)
print(videoTrack.timeRange.duration.description)
So you can change this line.
[videoComposition insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:sourceVideoTrack atTime:kCMTimeZero error:nil] ;
To like this.
[videoComposition insertTimeRange:sourceVideoTrack.timeRange, ofTrack:sourceVideoTrack atTime:kCMTimeZero error:nil] ;
For swift 5
videoComposition.insertTimeRange(sourceVideoTrack.timeRange, of: sourceVideoTrack, at: CMTime.zero)
Then you will avoid the last black frame :)
Hope this helps someone still suffer.
Just wanted to write this for people with my specific issue.
I was taking a video and trying to speed it up / slow it down by taking an AVMutableComposition and scaling the time range of the audio and video components via scaleTimeRange
Scaling the time range to 2x speed or 3x speed sometimes caused the last few frames of the video to be black. Fortunately, #Khushboo's answer fixed my problem as well.
However, instead of decreasing the exporter's timeRange by a few nanoseconds, I just made it the same as the composition's duration which ended up working perfectly.
exporter?.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: composition.duration)
Hope this helps!

some criteria peg CPU using Scripting Bridge and NSPredicate

I'm trying to get a list of tracks out of iTunes via Scripting Bridge. I'm using NSPredicate because that's the recommended way. This works very well in some cases, and is unusably slow in others. For instance, this will execute very quickly:
NSString *formatString = #"artist == ABC AND album == XYZ";
NSPredicate *trackFilter = [NSPredicate predicateWithFormat:formatString];
NSArray *tracksToPlay = [[libraryPlaylist fileTracks] filteredArrayUsingPredicate:trackFilter];
(libraryPlaylist is an iTunesLibraryPlaylist object that was created elsewhere.)
But if I add either kind or videoKind to the mix, iTunes hits 100% CPU for a minute or more.
NSString *formatString = #"artist == ABC AND album == XYZ AND kind != 'PDF document' AND videoKind == %#", ;
NSPredicate *trackFilter = [NSPredicate predicateWithFormat:formatString, [NSAppleEventDescriptor descriptorWithTypeCode:iTunesEVdKNone]];
NSArray *tracksToPlay = [[libraryPlaylist fileTracks] filteredArrayUsingPredicate:trackFilter];
But that will eventually work. The real failure is albumArtist. If I try
NSString *formatString = #"albumArtist == ABC AND album == XYZ";
NSPredicate *trackFilter = [NSPredicate predicateWithFormat:formatString];
NSArray *tracksToPlay = [[libraryPlaylist fileTracks] filteredArrayUsingPredicate:trackFilter];
iTunes will go to 100% CPU and sit there for I don't know how long. (I gave up after 3 or 4 minutes.) Am I missing something or is this a bug in iTunes?
Additional info
My code takes the resulting tracks and calls another method to add them to a playlist (also using Scripting Bridge). I noticed when trying to filter by kind, the tracks would slowly pop onto the list one by one while iTunes hammered the CPU. This can only mean that filteredArrayUsingPredicate has already returned its results, so what is iTunes working so hard on?
Another post indirectly helped me find the answer.
Using the “Library” playlist causes a number of unusual problems. Using the “Music” playlist instead seems to fix them. In the example above, setting libraryPlaylist this way is what caused the problem:
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
iTunesSource *library = [[[[iTunes sources] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %i", iTunesESrcLibrary]] objectAtIndex:0];
iTunesLibraryPlaylist *libraryPlaylist = [[[library libraryPlaylists] objectAtIndex:0];
Getting the "Music" playlist instead of the "Library" playlist is the answer:
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
iTunesSource *library = [[[[iTunes sources] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %i", iTunesESrcLibrary]] objectAtIndex:0];
iTunesLibraryPlaylist *libraryPlaylist = [[[[library playlists] get] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"specialKind == %i", iTunesESpKMusic]] objectAtIndex:0];
Other things to be aware of
The "albumArtist == ABC AND album == XYZ" filter in the original question was actually running pretty quickly. What's slow is anything you do with the result afterward. Calling get right away is a partial solution. (get runs as slow as anything else, but by doing it up front, you limit the slowness to a single operation. Also note that get only works on an SBElementArray.)
I've also found that calling fileTracks re-introduces some slowness. Using tracks instead fixes that. So the filter should read:
NSArray *tracksToPlay = [(SBElementArray *)[[libraryPlaylist tracks] filteredArrayUsingPredicate:trackFilter] get];
(When using "Library", only fileTracks would return objects with a location property, which you need to add them to a playlist. After switching to "Music", tracks seems to return objects with a location as well.)

Audio on TouchesEnded

On TouchesEnded I want to do two things:
1) return the view to its original position - this works fine.
2) I want to play an audio sound if the touch ended on the YES vies and a different sound for the NO views.
How can I do this?
At this moment my sound for NO will play for every touch ended - and this is not good.
I am new to this so please explain the basics :) my current code below.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
yesletter1.frame = keepYesLetter1Place;
yesletter2.frame = keepYesLetter2Place;
yesletter3.frame = keepYesLetter3Place;
yesletter4.frame = keepYesLetter4Place;
notletter1.frame = keepNoLetter1Place;
notletter2.frame = keepNoLetter2Place;
notletter3.frame = keepNoLetter3Place;
notletter4.frame = keepNoLetter4Place;
NSString *path = [[NSBundle mainBundle]
pathForResource:#"no"ofType:#"wav"];
AVAudioPlayer* theAudio = [[AVAudioPlayer alloc]
initWithContentsOfURL:[NSURL fileURLWithPath:path] error:NULL];
[theAudio play];
}
Solved this issue not the way i wanted but thing work now.
I have 2 different sounds for some objects one sound and the other another sound.
What i did is added a flag for each object and did if(flag1 == 0 || flag2 == 0 ect...) play sound 1 else play sound 2.
in another method which does something else i played with the flags.
I am sure there is a better solution for this :) but things work now.

create iTunes playlist with scripting bridge

I am trying to create a new user playlist using the cocoa scripting bridge, but cannot seem to get it to work. I have so far:
iTunesApplication *iTunes = [SBApplication
applicationWithBundleIdentifier:#"com.apple.iTunes"];
SBElementArray *iSources = [iTunes sources];
iTunesSource *library = nil;
for (iTunesSource *source in iSources) {
if ([[source name] isEqualToString:#"Library"]) {
library = source;
break;
}
}
// could not find the itunes library
if (!library) {
NSLog(#"Could not connect to the iTunes library");
return;
}
// now look for our playlist
NSString *playlistName = #"new playlist";
SBElementArray *playlists = [library userPlaylists];
iTunesUserPlaylist *playlist = nil;
for (iTunesUserPlaylist *thisList in playlists) {
if ([[thisList name] isEqualToString:playlistName]) {
playlist = thisList;
break;
}
}
// if the playlist was not found, create it
if (!playlist) {
playlist = [[[iTunes classForScriptingClass:#"playlist"] alloc] init];
[playlist setName:playlistName];
[[library userPlaylists] insertObject:playlist atIndex:0];
}
When I try and add a name for the playlist, I get the error message:
iTunesBridge[630:80f] *** -[SBProxyByClass setName:]: object has not been added to a container yet; selector not recognized
Can anyone point me in the correct direction?
The error message is telling you that Scripting Bridge objects like your playlist can't receive messages until they've been added to the relevant SBElementArray, so your attempt to set a property on the playlist before adding it to the array fails.
The simplest solution is just to rearrange the last two lines of code, like this:
// if the playlist was not found, create it
if (!playlist) {
playlist = [[[iTunes classForScriptingClass:#"playlist"] alloc] init];
[[library userPlaylists] insertObject:playlist atIndex:0];
[playlist setName:playlistName];
}
The other option is to use initWithProperties: which according to your comment on another answer is what you ended up doing.
Making new application objects is dreadfully obfuscated in SB. The pseudo-Cocoa-ish alloc-init-insert procedure bears no resemblance to what's actually going on underneath. While the alloc-init appears to create a regular object that you can manipulate with subsequent method calls, the result is actually a shim whose only function is to be 'inserted' into an 'array', at which point SB sends an actual make event to the target process. (See also here and here for SB criticisms.)
IIRC, the only point you can actually specify initial properties is in -initWithProperties:. You can set them after the object has been 'inserted', but that is a completely different operation (manipulating an object that already exists rather than specifying initial state for an object being created) so can easily have unintended consequences if you aren't careful.
At any rate, here's how you'd normally create a new playlist if one doesn't already exist:
set playlistName to "new playlist"
tell application "iTunes"
if not (exists playlist playlistName) then
make new playlist with properties {name:playlistName}
end if
end tell
And, FWIW, here's how I'd do it in ObjC, using objc-appscript (which I wrote so I wouldn't have to use SB, natch):
#import "ITGlue/ITGlue.h"
NSString *playlistName = #"new playlist";
ITApplication *itunes = [ITApplication applicationWithName: #"iTunes"];
ITReference *playlist = [[itunes playlists] byName: playlistName];
if ([[[playlist exists] send] boolValue])
playlist = [playlist getItem];
else
playlist = [[[[itunes make] new_: [ITConstant playlist]]
withProperties: [NSDictionary dictionaryWithObject: playlistName
forKey: [ITConstant name]]] send];
(The downside of objc-appscript is that you have to build and embed a copy of the framework in your application bundle. The benefits are that it's more capable, less prone to application compatibility issues, and much less obfuscated. Plus you can use appscript's ASTranslate tool to convert the Apple events sent by the above AppleScript into ObjC syntax - very handy when figuring out how to construct your references and commands.)
Just a quick note that [[source name] isEqualToString:#"Library"] definitely does not work on non-english systems. It might be better to simply use iTunesSource *library = [[_iTunes sources] objectAtIndex: 0]; since the first source item is the one at the top, e.g. the main library.
This is what I've done to reliably identify the library. I could be doing it wrong.
- (iTunesSource *)iTunesLibrary
{
NSArray *librarySource = [[[self iTunes] sources] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"kind == %#", [NSAppleEventDescriptor descriptorWithTypeCode:iTunesESrcLibrary]]];
if ([[librarySource lastObject] exists]) {
return [librarySource lastObject];
}
return nil;
}
You should look into EyeTunes. It's an open-source framework for interacting with iTunes using Objective-C. You code would look much more simple if you did it through EyeTunes.
http://www.liquidx.net/eyetunes/

Resources