9

EDIT: The culprit was iOS 8, not the simulator (which I didn't realize was already running iOS 8) I've renamed the title to reflect this.

I was happily using the code from this SO question to load album artwork from mp3 files. This was on my iPhone 5 with iOS 7.1.

But then I traced crashing in the iOS simulator to this code. Further investigation revealed that this code also crashed on my iPad. It crashed on my iPad after upgrading it to iOS 8.

It appears the dictionary containing the image is corrupted.

I created a dummy iOS project that only loads album art and got the same result. Below is the code from that viewcontroller.

- (void) viewDidAppear:(BOOL)animated
{
    self.titleText = @"Overkill"; // Set song filename here
    NSString *filePath = [[NSBundle mainBundle] pathForResource:self.titleText ofType:@"mp3"];
    if (!filePath) {
        return;
    }
    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    NSLog(@"Getting song metadata for %@", self.titleText);
    AVAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
    if (asset != nil) {
        NSArray *keys = [NSArray arrayWithObjects:@"commonMetadata", nil];
        [asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
            NSArray *artworks = [AVMetadataItem metadataItemsFromArray:asset.commonMetadata
                                                               withKey:AVMetadataCommonKeyArtwork
                                                              keySpace:AVMetadataKeySpaceCommon];
            UIImage *albumArtWork;

            for (AVMetadataItem *item in artworks) {
                if ([item.keySpace isEqualToString:AVMetadataKeySpaceID3]) {
                    NSDictionary *dict = [item.value copyWithZone:nil];

                    // **********
                    // Crashes here with SIGABRT. dict is not a valid dictionary.
                    // **********

                    if ([dict objectForKey:@"data"]) { 
                        albumArtWork = [UIImage imageWithData:[dict objectForKey:@"data"]];
                    }
                }
                else if ([item.keySpace isEqualToString:AVMetadataKeySpaceiTunes]) {
                    // This doesn't appear to get called for images set (ironically) in iTunes
                    albumArtWork = [UIImage imageWithData:[item.value copyWithZone:nil]];
                }
            }

            if (albumArtWork != nil) {
                dispatch_sync(dispatch_get_main_queue(), ^{
                    [self.albumArtImageView setImage:albumArtWork];
                });
            }

        }];
    }

}

I've marked the line with the crash. It expects a file Overkill.mp3 to be in the bundle. I tested with multiple mp3's and m4a's exported from iTunes and Amazon, so I know the files themselves are correctly encoded.

Tested in Xcode 6.0 and 6.1.

Any ideas why it would work on iPhone but not the simulator or iPad?

EDIT / UPDATE:

Logging the item.value reveals differences.

On iPhone 5 (works):

(lldb) po item.value
{
    MIME = JPG;
    data = <ffd8ffe0 .... several Kb of data ..... 2a17ffd9>;
    identifier = "";
    picturetype = Other;
}

On Simulator (crashes)

(lldb) po item.value
<ffd8ffe0 .... several Kb of data ..... 2a17ffd9>

So it appears that on the simulator there is no dictionary, just the raw artwork.

Changing the code to not expect a dictionary, but take item.value as a UIImage works!

        for (AVMetadataItem *item in artworks) {
            if ([item.keySpace isEqualToString:AVMetadataKeySpaceID3]) {
                NSData *newImage = [item.value copyWithZone:nil];
                albumArtWork = [UIImage imageWithData:newImage];
            }
            ...
        }
Community
  • 1
  • 1
Stan James
  • 2,535
  • 1
  • 28
  • 35
  • Ah… Xcode 6.0. Must tell you, I wrote that code when I was still a beginner. A lot has changed since then. Try logging `item.value`, `item` and the dictionary created by copying `item.value`. Also, could you please post the backtrace and crash log itself? – duci9y Sep 11 '14 at 13:59
  • @duci9y Thanks for the direction to look! See my update above. Seems item.value is the raw UIImage data (and not a NSDictionary) when using the simulator or iPad. Very strange. Do you know a way to determine UIImage vs NSDictionary without crashing the app? (I'm looking into this now.) – Stan James Sep 11 '14 at 18:14
  • Use `AVMetadataID3MetadataKeyAttachedPicture` for MP3s and `AVMetadataiTunesMetadataKeyCoverArt` for iTunes instead of the common key and see if you get more predictable results. – duci9y Sep 11 '14 at 18:22

1 Answers1

17

It seems the returned data structure has changed in iOS 8. The value of the AVMetadataItem object is no longer a dictionary, but the actual raw UIImage data.

Adding a test for the NSFoundationVersionNumber solves the problem. There is probably a cleaner solution.

- (void) viewDidAppear:(BOOL)animated
{
    self.titleText = @"Overkill";
    NSString *filePath = [[NSBundle mainBundle] pathForResource:self.titleText ofType:@"mp3"];
    if (!filePath) {
        return;
    }
    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    NSLog(@"Getting song metadata for %@", self.titleText);
    AVAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
    if (asset != nil) {
        NSArray *keys = [NSArray arrayWithObjects:@"commonMetadata", nil];
        [asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
            NSArray *artworks = [AVMetadataItem metadataItemsFromArray:asset.commonMetadata
                                                               withKey:AVMetadataCommonKeyArtwork
                                                              keySpace:AVMetadataKeySpaceCommon];
            UIImage *albumArtWork;

            for (AVMetadataItem *item in artworks) {
                if ([item.keySpace isEqualToString:AVMetadataKeySpaceID3]) {

                    // *** WE TEST THE IOS VERSION HERE ***

                    if (TARGET_OS_IPHONE && NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1) {
                        NSData *newImage = [item.value copyWithZone:nil];
                        albumArtWork = [UIImage imageWithData:newImage];
                    }
                    else {
                        NSDictionary *dict = [item.value copyWithZone:nil];
                        if ([dict objectForKey:@"data"]) {
                            albumArtWork = [UIImage imageWithData:[dict objectForKey:@"data"]];
                        }
                    }
                }
                else if ([item.keySpace isEqualToString:AVMetadataKeySpaceiTunes]) {
                    // This doesn't appear to get called for images set (ironically) in iTunes
                    albumArtWork = [UIImage imageWithData:[item.value copyWithZone:nil]];
                }
            }

            if (albumArtWork != nil) {
                dispatch_sync(dispatch_get_main_queue(), ^{
                    [self.albumArtImageView setImage:albumArtWork];
                });
            }

        }];
    }

}
Stan James
  • 2,535
  • 1
  • 28
  • 35
  • 1
    Thank you so much Stan! Sometimes I feel like hopping on a plane to SF and throttling a few apple employees. Why they feel the need to waste our time with this sort of nonsense I'll never understand. – amergin Sep 27 '14 at 15:35
  • 1
    I think you can understand them when you have developed your own OS, and released a few versions of it. :) – Fahri Azimov Dec 15 '14 at 05:26