Bug: MPMediaItem Download Status on iOS & MPMediaPropertyPredicate using assetURL - ios

Fun bug in the MPMedia API
I have had an ongoing bug in my music app that I have finally tracked down now (now that I am re-writing it in swift). It has a few facets. (using systemMusicPlayer)
I think I have narrowed the issues down to an MPMediaItem that has the following properties.
MPMediaItemPropertyIsCloudItem = true
assetURL = nil
** these two make sense, but the following corner case (well probably pretty common) threw me for a while**
The first 2 items can be true, but I believe if you copied it from iTunes, it /can/ be playable (they would play most of the time for me), and there is no way to tell. I have tested this over and over and it seems to be the case, but these MPMediaItems by their existence might only sometimes cause issue, or they are fine. But you cant find out which songs these are.
If you are playing a queue, and systemMusicPlayer comes across a
song in your library but not downloaded or copied form iTunes, I believe it will automatically skip the
song, similar to systemMusicPlayer.skipToNextItem(), but I think
internally it's a different mechanism.
2a. This behavior causes a basically unrecoverable problem if you are using systemMusicPlayer.skipToPreviousItem() and come across an Item that would have been skipped over - meaning, it doesn't recognize that you are trying to move back in the queue and just throws the error and moves the queue forward.
2b. As far I I could tell, when the error hits going forward the MPMediaItem never becomes the nowPlayingItem. The problems going backwards get compounded by the MPMediaItem metadata (which is always available weather it is local or not) getting loaded, but the song trying to play immediately sends it forward in the queue again.
OK, so asinine and infuriating.
Now to my question:
I cant do anything about not being able to know if a cloud item is on the device or not (via iTunes). I /should/ be able to just filter out if an item has an assetURL, however, which is a guarantee that it is local and available.
let filter:MPMediaPropertyPredicate = MPMediaPropertyPredicate(value: "ipod", forProperty: MPMediaItemPropertyAssetURL, comparisonType: MPMediaPredicateComparison.Contains)
This returns 0 items. Does anyone know of a way to filter on this property? Doing it here seems like it would be the cleanest, and should leave the query returning items and itemSections. All my tables populate from the queries, and I dont think theres a way to reconstruct one manually.
The URL has a format like this: ipod-library://item/item.m4a?id=5314739480586915369
Now, I suspect it is possible to add catches when populating table views and such, but it feels really messy.
This is ios 9.2.1, Swift 2, Xcode 7.2.1
I have not yet wiped the phone and re-copied the songs. Manually downloading them from the Music app is the only way the items get an assetURL if it was not present.

It's not as efficient, but one thing you can do:
let query = MPMediaQuery()
let allItems = query.items ?? []
let items = allItems.filter { $0.assetURL?.scheme?.hasPrefix("ipod") ?? false }

From MPMediaItem.h, you can see only these are filterable(commented with filterable):
MPMediaItemPropertyPersistentID
MPMediaItemPropertyMediaType
MPMediaItemPropertyTitle
MPMediaItemPropertyAlbumTitle
MPMediaItemPropertyAlbumPersistentID
MPMediaItemPropertyArtist
MPMediaItemPropertyArtistPersistentID
MPMediaItemPropertyAlbumArtist
MPMediaItemPropertyAlbumArtistPersistentID
MPMediaItemPropertyGenre
MPMediaItemPropertyGenrePersistentID
MPMediaItemPropertyCompose
MPMediaItemPropertyComposerPersistentID
MPMediaItemPropertyIsCompilation
MPMediaItemPropertyIsCloudItem
MPMediaItemPropertyHasProtectedAsset
MPMediaItemPropertyPodcastTitle
MPMediaItemPropertyPodcastPersistentID
MPMediaItemPropertyPlayCount
So it is impossible to build any query with condition on assetURL property. It is a dead end trying to do anything on assetURL unless you fetch all the MPMediaItems and do a NSArray search.
Also from somewhere in the Apple's docs, I remember vaguely, you can not by any means get the information of where a cloud item is downloaded or not.
However if you want to investigate more into cloud/local issues, I suggest you take consideration of user's music setting on if iCloud Music Library is turned on or off and looks into MPMediaItemPropertyHasProtectedAsset.
If the purpose is to detect if the song is a local song or not, you can just build a query on both isCloudItem == FALSE AND hasProtectedAsset == FALSE, in that case assetURL does not matter.

Related

Swift: Is there an OS call in macOS/iOS similar to NSSpellCheck but for getting a word definition from the dictionary,

I can spell check words like this:
import Cocoa
let words = ["happy","flingey","bookinto"]
let spellCheck = NSSpellChecker()
for word in words {
if spellCheck.checkSpelling(of: word, startingAt: 0).length == 0 {
print("Word: \(word) is good")
} else {
print("Word: \(word) is bad")
}
}
However, the output is:
Word: happy is good
Word: flingey is bad
Word: bookinto is good
There are some (only some) combinations of valid words, like "book into" but with a missing space which the spellchecker still accepts as valid.
I suspect I might be able to tweak how NSSpellChecker operates to eliminate the issue, however, if I can get the next part working, I won't need to worry about this one.
For a later part of the App (a quiz) I want to let the user see a definition of the word within the App. Something like this
import Cocoa
let definer = NSDictionaryDefinition()
let word = "Happy"
if let meaning = definer.getDefinition(for: word) {
print("\(word): \(meaning.getText())")
}
Which would then give:
Happy: feeling or showing pleasure or contentment
NSDictionaryDefinition is just something I made up as the kind of API name I was expecting to find, and obviously given how macOS APIs work it won't be as easy as that, it will probably be within a collection within a container within a callback, or whatever, but I'm happy dealing with all that when I get there, my problem is I can't find any system API that remotely resembles something like NSDictionaryDefinition.
My web search is hampered by the fact that Dictionary is also a Swift Collection Type, so I get hundreds of false hits, but even trawling through many of those, I didn't get lucky.
So next I started looking in the API references on the Apple developer site, which is just as painful because it's so vast and sprawling with little by way of a route map through it, but even then I couldn't locate anything.
Does anyone know if this system API exist for Swift (or even Objective-C). I've always assumed it did because the dictionary comes as standard in macOS and most things in macOS are programable via an API. It needs to be a system native API to work offline and also localise to the user automatically.
Please don't suggest third party resources, as I notice a previous question about that got marked as off-topic here. I'm assuming this question is on topic but I genuinely struggle to gauge it.
Thanks.
Something like this exists on iOS. Look into UIReferenceLibraryViewController. It doesn't give you back the definitions as strings, but you can check if there is a definition for a word and you can show the system-provided dictionary view.

How to fix 'sqlite3' and other errors in CallKit Directory extension?

The idea of the app is to add around 50.000 contacts to the CallKit Directory, but when I try to add them with context.addIdentificationEntry(withNextSequentialPhoneNumber: contact.number, label: contact.contactLabel) in the beginRequest(with context: CXCallDirectoryExtensionContext) method i often either get a com.apple.CallKit.error.calldirectorymanager error 2 or some strange sqlite3 error:
sqlite3_step for query 'INSERT INTO PhoneNumberIdentificationEntry
(extension_id, phone_number_id, label_id) VALUES (?, (SELECT id
FROM PhoneNumber WHERE (number = ?)), (SELECT id FROM Label WHERE
(localized_label = ?))),
…
(SELECT id FROM Label WHERE (localized_label = ?)))' returned 19
(2067) errorMessage 'UNIQUE constraint failed:
PhoneNumberIdentificationEntry.extension_id,
PhoneNumberIdentificationEntry.phone_number_id,
PhoneNumberIdentificationEntry.label_id'
Generally I fetch the contacts from a server and try to keep them in sync - therefore, I save them locally with Realm. Has anyone ideas how to handle such errors?
Since I could not find much solutions to CallKit Directory errors, I tried to fix the errors by myself. It took me some time and to save some time for other developers I try to sum up all errors I got during the implementation of the CallKit Directory Extension with a suggested solution for each of them - I cannot guarantee that these solutions work for 100% but maybe they still can help!
sqlite3_step error: I often got the error when I tried to sync the contacts again, therefore I guess, that the error is some kind of duplicate error. Since it is possible that a contact has two numbers, I used the same name for both entries which basically worked, but I think there is still some kind of identification problem in the background if you use the exact same name for the label. Therefore, I added the index of each contact at the end of the label to keep them unique and now the error does not appear again anymore. (Even if all of your contacts have only one number keep in mind, that there are maybe some contacts with the same first and last name.)
com.apple.CallKit.error.calldirectorymanager error 2: This error appears often due to memory problems. Extension have much less memory available than the app itself (CallKit Directory Extension has for example only 12 MB available). To prevent this error I used a paging mechanism which always only loads a specific amount of contacts into the extension and then gets reloaded with the next page. If you use Realm there are additionally two aspects to consider: First, set the used object types in the Realm configuration Realm(configuration: Realm.Configuration(fileURL: realmFileURL, objectTypes: [ContactObject.self])) - this prevents Realm from calling objc_copyClassList() which needs a lot of memory. Second, if you filter the realm objects which a specific predicate try to avoid using any kind of references in the filter - for example, first, after adding the contact to the CallKit Directory, I passed the number of the contact to the filter of the realm objects to mark that contact as synced. But then I saw that the memory in the extension increased with each synced contact since there were probably some kind of references to the contact object. Then, I fetched the exact same page of contacts to mark them all as synced instead of fetching them one by one which allowed me to load much more contacts per page. Tip: If you want to use Breakpoints in the extension or want to debug the memory of the extension go in Xcode to Debug -> Attach to Process -> AppName Call Directory. Then you can select the call directory in the debug navigator and see the used amount of memory. (You can only attach to the extension process when it is currently used - just in case you can’t see it in the list)
com.apple.CallKit.error.calldirectorymanager error 3: The contact numbers are not ordered - order them before adding them to the directory.
com.apple.CallKit.error.calldirectorymanager error 4: Duplicated Entries - ensure to add a number only once.
Please do not hesitate to correct me if I am wrong with any of these suggestions or to add further tips and solutions - otherwise I am glad if I could help!
It seems like you're adding duplicate contacts. Just make sure the contacts you're adding are unique.

Unusual UITableView crash on -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array

I've searched a ton and haven't found an answer that seems to address the problem I'm having.
I have a UITableViewController. In the viewDidLoad method I load objects from a database and then call reloadData.
Now generally this works fine. However, now I'm implementing sorting, which is persisted so that the same sorting algorithm is used on the next app launch. Note in all cases, the size of the array backing the section is the same after sorting.
If I use the default sorting method, I get no crash, even though it runs through the same sort > call delegate > reloadTable code path. However, if I use an alternate sorting method, which uses the same code path, I see the table view ask for number of rows, and get the correct number, but then I get this exception. Only the objects are sorted in a different order, so this makes no sense to me.
Also of note is that if I sort the objects (which uses the same code path) after the table has been displayed, it works fine.
This makes no sense to me, and almost seems like a UITableView bug, but I hope it's something I can fix.
My app is open source, so I can link directly to the relevant files:
https://github.com/einsteinx2/iSub/blob/weird_crash/Classes/UI/Reusable/ItemViewController.swift
https://github.com/einsteinx2/iSub/blob/weird_crash/Classes/UI/Reusable/ItemViewModel.swift
The most relevant methods are ItemViewModel.sortAll() and ItemViewController.itemsChanged(viewModel: ItemViewModel)
I've run out of ideas on how to debug this. It just makes no sense.
Some trial and error things I've tried:
Using only one table section
Calling reloadData after a delay to rule out some kind of race condition
Not calling reloadData in my itemsChanged delegate method (still crashes, as reloadData seems to be called automatically by the table view controller)
Nothing seems to make any difference. I'm completely confused how this could be possible. If anyone has any ideas on how I can continue to debug this, or how it may be possible, I'm all ears. Thanks.
EDIT: To make matters more confusing, I have similarly persisted sorting for albums, which use the same ItemViewController, however returning to an album that was previously sorted does not cause a crash, even though it should be doing the exact same thing and running the same code path.
Wow, I finally figured it out and talk about edge case!
Turns out the crash has to do with the table section indexes (the letters on the right hand side), not the actual data loading or sorting.
I was using the unicode bullet character • when not sorted by name. Somehow this worked when you sorted live, but there is some bug in UITableView that causes the internal array of section indexes to be empty when you load the table with that data. I switched to using the black circle character ● and now it no longer crashes.
I think this has to be the smallest change for the most amount of investigation work I've ever done in 8 years of coding for iOS.
Hope this helps some random person in the future. I'll report a bug to Apple, though likely it won't be high priority so who knows if/when they'll fix it.

AVCaptureSession audio doesn't work for long videos

I'm using AVCaptureSession to record a video with audio. Everything seems to work properly for short videos, but for some reason, if I record a video that is longer than about 12 seconds, the audio doesn't work.
Edit (because this answer is still getting upvotes): This answer works to mitigate the problem but the likely root cause for the issue is addressed in #jfeldman's answer.
I found the solution as an answer to a completely different question.
The issue is the movieFragmentInterval property in AVCaptureMovieFileOutput.
The documentation for this property explains what these fragments are:
A QuickTime movie is comprised of media samples and a sample table
identifying their location in the file. A movie file without a sample
table is unreadable.
In a processed file, the sample table typically appears at the
beginning of the file. It may also appear at the end of the file, in
which case the header contains a pointer to the sample table at the
end. When a new movie file is being recorded, it is not possible to
write the sample table since the size of the file is not yet known.
Instead, the table is must be written when recording is complete. If
no other action is taken, this means that if the recording does not
complete successfully (for example, in the event of a crash), the file
data is unusable (because there is no sample table). By periodically
inserting “movie fragments” into the movie file, the sample table can
be built up incrementally. This means that if the file is not written
completely, the movie file is still usable (up to the point where the
last fragment was written).
It also says:
The default is 10 seconds. Set to kCMTimeInvalid to disable movie
fragment writing (not typically recommended).
So for some reason my recording is getting messed up whenever a fragment is written. I just added the line movieFileOutput.movieFragmentInterval = kCMTimeInvalid; (where movieFileOutput is the AVCaptureMovieFileOutput I've added to the AVCaptureSession) to disable fragment writing, and the audio now works.
We also experienced this issue. Basically disabling movie fragment writing will work but it doesn't actually explain the issue. Most likely you are recording to an output file using a file extension that does not support this feature, like mp4. If you pass an output file with the extension mov you should have no issues using movie fragment writing and the output file will have audio.
Updating videoFileOutput.movieFragmentInterval = kCMTimeInvalid solved this for me.
However, I accidentally set the movieFragmentInterval after calling startRecordingToOutputFileURL. An agonizing hour later I realized my mistake. For newbies like me, note this obvious sequence.
videoFileOutput.movieFragmentInterval = kCMTimeInvalid
videoFileOutput.startRecordingToOutputFileURL(filePath, recordingDelegate: recordingDelegate)
kCMTimeInvalid is now deprecated. This is how to assign it now:
videoFileOutput?.movieFragmentInterval = CMTime.invalid

HTML5 video seeking on iPad

I have an HTML5 video player with a custom seek bar, that's working great on the iPhone (playing inline) and on the browser.
It plays great on the iPad too and the seek bar is updated as the movie plays, but for some reason, I can't seek.
All of the values are correct and I'm trying to set:
myPlayer.currentTime = XX;
Unfortunately, the iPad refuses to set the .currentTime attribute.
From what I can gather the difference between the browser and iPad is that on the browser I get:
myPlayer.networkState = 3
myPlayer.readyState = 4
On the iPad I get:
myPlayer.networkState = 2
myPlayer.readyState = 3
It's exactly the same code, running a local MP4 video.
Any idea why this is happening?
Cheers,
Andre
I've had all kinds of problems getting JavaScript to control audio elements, and a lot of frustration with the currentTime property, along with Apple's restrictions on what constitutes direct user initiation of events.
It wouldn't surprise me if there were some kind of weird bug with JavaScript & HTML5 video playback on the iPad (or "feature" that's undocumented), which requires a workaround. From my experience, the iPad has a unique way of doing things than what's in the official documentation.
You should check the error, buffered, seekable, and seeking properties of the video element. Looking at your readyState & networkState values, the iPad seems to think that the video has not been completely loaded - which is odd for a local resource.
buffered and seekable should be equal to the time range of your entire video. seeking should be TRUE. That should at least give you a little more information about the problem.
Have you tested it with other videos? It might be that there is some kind of encoding problem with the video that the iPad has a problem with.
Other than that - there was a bug in a previous iPad OS version that broke the ability to set the currentTime property. Are you using the latest OS version?
This issue is related with value used on the video.currentTime property. On my specific case I fixed the problem by always making sure I was using floating point numbers with 1 decimal digit during the seek.
Setting video.currentTime to ZERO on iOS 3.2 will indeed seek the video to the beginning but the value won't update after that - timeupdate event is still dispatched normally but if you try to read currentTime it will always return the same value.
To seek to the begin of the video use 0.1 instead of 0, to seek to 12.2345 use 12.2.
PS: you can use (+(12.2345).toFixed(1)) to limit the number of decimal digits to 1.
Kyle's answer is a good one. I would add, you can't assume the seekable attribute is filled in after any particular event. This means you can't wait for events like loadedmetadata, canplay, or any other and assume that you're able to set currentTime safely at that point.
It seems the safest way to do it is to check seekable after every video-related event, and if it encompasses the time you want to seek to, set currentTime at that point. On iPad, seekable may not be filled until after the canplaythrough event, which is quite late.
See my blog post for more on this.
I am having the same issue - here are the properties in my case:
UIWebView - iPad Simulator
duration=4.861666679382324
startTime=0
currentTime=4.861666679382324
buffered(1)=[0-0]
seekable(0)=
seeking=false
error=null
readystate=4
networkstate=3
Chrome:
duration=4.9226298332214355
startTime=0
currentTime=4.9226298332214355
buffered(1)=[0-4.9226298332214355]
seekable(1)=[0-4.9226298332214355]
seeking=false
error=null
readystate=4
networkstate=1
so - nothing is getting buffered and nothing is seekable. i am playing a local clip from the resources directory of an iPad bundle, via a UIWebView.
In my case, all i need is to reset to the top of the video after each play, and I was able to accomplish this via a call to "load()"

Resources