I'm trying to get the asset property from an AVAssetTrack object, but it's nil sometimes. It seems like the problem occurs only after I use Dispatch.main.async.
According to the documentation, it's necessary to use loadValuesAsynchronously(forKeys:, completion:) to avoid blocking the main thread, and return to the main thread after loading is done.
let asset = AVURLAsset(url: videoInAppBundleURL)
let track = asset.tracks(withMediaType: .video).first!
assert(track.asset != nil) // passes
track.loadValuesAsynchronously(forKeys: [#keyPath(AVAssetTrack.asset)]) {
assert(track.asset != nil) // passes
DispatchQueue.main.async {
assert(track.asset != nil) // FAILS
// [...]
}
}
What I found out is:
It makes no difference whether I'm running on a device or the
simulator.
It seems not to be a problem with the video / videoURL.
The video is part of the main bundle, I tried both .mp4 and .mov
files and I made sure the video works by displaying it via an
AVPlayerViewController.
Here is a working demo project.
I'm also wondering: why is AVAssetTrack's asset property optional? (all!! the other properties are non optional)
Note: this question has been edited after reading Matt's helpful comments and further investigation.
I reproduced the issue, with some tweaking of your github example, like this:
let asset = AVURLAsset(url: videoInAppBundleURL)
let tracksKey = #keyPath(AVAsset.tracks)
asset.loadValuesAsynchronously(forKeys: [tracksKey]) {
let track = asset.tracks(withMediaType: .video).first!
DispatchQueue.main.async {
assert(track.asset != nil) // fails
}
}
Okay, but now watch closely as I perform an amazing trick:
let asset = AVURLAsset(url: videoInAppBundleURL)
let tracksKey = #keyPath(AVAsset.tracks)
asset.loadValuesAsynchronously(forKeys: [tracksKey]) {
let track = asset.tracks(withMediaType: .video).first!
DispatchQueue.main.async {
print(asset) // <-- amazing trick
assert(track.asset != nil) // passes!
}
}
Whoa! All I did was add a print statement — and now suddenly the very same assertion passes. This in fact is parallel to your original statement (which you later edited out) that "Sometimes the problems are gone, when stepping through the code with the debugger.”
So, now, my suspicions being thoroughly aroused, I did something unbelievably clever (even if I do say so myself). I removed the print(asset), but I switched the scheme’s configuration from Debug to Release. Presto, the assertion still passes.
So what you’ve found is a quirk of the compiler — dare I call it a bug?
But wait, there’s more. You asked, quite reasonably, why asset is Optional. It’s because it’s weak:
weak open var asset: AVAsset? { get }
So there’s your answer. The track has only a weak reference to its asset. If we pass the track down into an asynchronous queue, and we do not bring the asset itself along with us, then the weak reference lets go and the asset is lost — in a Debug build.
Hope this helps. You are probably waiting for me to make some grand conclusory statement about whether this constitutes a bug, but I’m not going to, sorry. I’ve provided two workarounds (use a Release build, or deliberately carry the asset reference down into the async queue) and that’s as far as I can go.
Related
Before I updated to iOS 14 on my iPhone, this code was working perfectly. After, iOS 14 this is weirdly not running... it is very odd and I have not seen any solution online, additionally from my investigation, I have not been able to see any change.
This code is used in order to retrieve a videoURL for this video from the imported Camera Roll (I use import Photos...).
phResourceManager.writeData(for: resource.last!, toFile: newURL!, options: resourceRequestOptions) { (error) in
if error != nil {
print(error, "not c67omplted error?")
} else {
print("woah completedd 345?")
newUserTakenVideo.videoURL = newURL
print(newUserTakenVideo.videoURL, "<--?")
}
}
EDIT:
To be clear, it "does not run" means the compleition block never runs... as in it never even runs and gives an error, the compleition block simply never is called (nothing prints at least..)
And here is a print statement printing out all the values I pass in to the parameters:
phResourceManager:
<PHAssetResourceManager: 0x282d352c0>
resource.last:
Optional(<PHAssetResource: 0x28128bc00> {
type: video
uti: public.mpeg-4
filename: v07044090000bu6n1nhlp4leque7r720.mp4
asset: C97B45D3-7039-4626-BA3E-BCA67912A2A9/L0/001
locallyAvailable: YES
fileURL: file:///var/mobile/Media/DCIM/113APPLE/IMG_3404.MP4
width: 576
height: 1024
fileSize: 4664955
analysisType: unavailable
cplResourceType: Original
isCurrent: YES
})
newURL:
Optional(file:///var/mobile/Containers/Data/Application/E2792F47-142E-4601-8D5B-F549D03C9AFE/Documents/Untitled%2027228354.MP4)
resourceRequestOptions:
<PHAssetResourceRequestOptions: 0x28230d480>
Note: this is the decleration for the resource variable:
let resource = PHAssetResource.assetResources(for: (cell?.assetPH)!)
I have a solution to this! Swift 4+, tested on iOS 14!
I looked through using a PHAssetResourceRequest, but the file names were messed with in the process, and it generally didn't work with my sandbox. Then I also tried requesting a AVPlayerItem from the PHAsset but this too, did not work with sandboxing...
But then, I tried simply using PHAssetResourceManager.default().writeData(... and seemingly started working!
I tested a bit more and seemed to work, here is the full code:
let resource = PHAssetResource.assetResources(for: (cell?.assetPH)!)
let resourceRequestOptions = PHAssetResourceRequestOptions()
let newURL = ExistingMediaVC.newFileUrl
PHAssetResourceManager.default().writeData(for: resource.last!, toFile: newURL!, options: resourceRequestOptions) { (error) in
if error != nil {
print(error, "error")
} else {
print("good")
newUserTakenVideo.videoURL = newURL
}
}
It is quite simple!! Tell me if anything is not working, and note I still use the ExisitingMedia.fileURL variable you used in your original code as well :)
I have seen many questions on this and none have had a solution. I am getting:
An AVPlayerItem can occupy only one position in a player's queue at a time
In the console after quickly taping a view button which should have one of two states:
Successfully preloaded video
only a videoURL which will then be loaded in another VC.
Why is this happening and how can I fix it?
This is my code which handels this in the transitioned to VC:
if numberMedia == 0 {
if selectedPost!.interimMedia[numberMedia].playerLayer != nil {
playCurrentMediaVid(currentMedia: selectedPost!.interimMedia[numberMedia])
} else {
let videoURL = selectedPost!.interimMedia[numberMedia].videoURL
if videoURL != nil {
//if we did not preload the video but have the cached vidURL
preloadVideo(media: selectedPost!.interimMedia[numberMedia])
playCurrentMediaVid(currentMedia: selectedPost!.interimMedia[numberMedia])
} else {
//if we dont have the cached vid url
selectedPost!.interimMedia[numberMedia].videoURL = getVideoURL(stringUrl: selectedPost!.interimMedia[numberMedia].videoURLString)
preloadVideo(media: selectedPost!.interimMedia[numberMedia])
playCurrentMediaVid(currentMedia: selectedPost!.interimMedia[numberMedia])
}
}
}
I looked into this problem and found the following problem:
If you print the status.rawValue of the playerQueue and currentItem (the AVPlayerItem) of the AVPlayerQueue (sub-class of AVPlayer) you'll find that the status will be either 0 or 1. For reference: 0 means status.unknown and 1 means status.readyToPlay (the one you want). So if both prints are 0 it fails, which is why you get the error. The position is at unknown, yet you are trying to .play() the playerQueue, which of course results in the error.
My recommendation to you (the fix) is to either find out a way to make the loading of the media inside the AVPlayerItem faster (which may be done putting the URL in a AVAsset instead of trying to immediately init a URL into a AVPlayer), or can use KVO to detect when it has finished loading (sort of like a completionBlock-esque solution).
OK. I have a nasty feeling that this will be met with the gentle chirp of crickets...
I base that on this and this.
I'm actually wondering if this is a feature, not a bug, as maybe there's a security issue with loading a movie locally, then playing it. I would think that isn't the case, but maybe. It should be noted that the loaded asset comes from a REST interaction with a server, in which the movie data is actually just a part of a data query response. It is not something that is loaded directly from a video streaming page (it is SSL, though).
I'm pretty green at AV Foundation.
I have the following code:
do {
// We create a path to a unique temporary file to grab the media.
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
// Store the media in the temp file.
try myData.write(to: url, options: .atomic)
let options = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
let asset = AVURLAsset(url: url, options: options)
if 0 < asset.tracks.count {
print("YOU GET \(asset.tracks.count) TRACKS!")
} else {
print("NO TRACKS FOR YOU!")
}
} catch let error {
NSLog("Error Encoding AV Media: %#", error._domain)
}
Pretty basic, eh? The "myData" variable contains a MP4 movie (.m4v) that was downloaded. I write it to a temp file, then load that temp file with AVURLAsset, just like it says to do.
The problem is that I can never get the dangblammit movie to play. The file is where it's supposed to be. I can fish out the temp file, slap on a '.m4v' extension, and play it in the QT Viewer.
I am quite prepared to accept a slap upside the head, followed by "ya darn eedjut!", but I'd like to know which "M" I should "RTFM".
The problem seems to be with this line
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
You should add the extension for the file.
let videoName = UUID().uuidString + ".mp4"
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(videoName)
Hope it fix.
I'm trying to write a simple Quick Look Preview Extension for my UIDocument-based iOS app.
The problem is that in my implementation of preparePreviewOfFile(at:completionHandler:) my attempt to open the UIDocument based on the URL I'm being handed is failing. I instantiate my document with the file URL and call open(completionHandler:) but I'm not getting any data, and I'm seeing a console message that the file coordinator has crashed.
All of this works fine in my actual app; it's just the Quick Look Preview Extension implementation that's having trouble. Is there something special I have to do to open a UIDocument from inside a Quick Look Preview Extension? Apple doesn't provide any sample code; in WWDC 2017 video 229 they just gloss over the whole thing.
EDIT: Curiouser and curiouser. I created a simplified testbed app that displays a Quick Look preview with UIDocumentInteractionController, along with my custom Quick Look Preview Extension. On the Simulator, the preview works! On the device, it doesn't. It looks like, when I tell my document to open, its load(fromContents:ofType) is never even called; instead, we are getting a pair of error messages like this:
The connection to service named com.apple.FileCoordination was invalidated.
A process invoked one of the -[NSFileCoordinator coordinate...] methods but filecoordinationd crashed. Returning an error.
I was able to work around the issue by not calling open on my UIDocument. Instead, I call read directly, on a background thread, like this:
func preparePreviewOfFile(at url: URL, completionHandler handler: #escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {
let doc = MyDocument(fileURL: url)
do {
try doc.read(from: url)
DispatchQueue.main.async {
// update interface here!
}
handler(nil)
} catch {
handler(error)
}
}
}
I have no idea if that's even legal. You'd think that just reading the document straight in, without the use of a file coordinator, would be Bad. But it does seem to work!
I found yet another workaround, using NSFileCoordinator and calling load manually to get the UIDocument to process the data:
let fc = NSFileCoordinator()
let intent = NSFileAccessIntent.readingIntent(with: url)
fc.coordinate(with: [intent], queue: .main) { err in
do {
let data = try Data(contentsOf: intent.url)
let doc = MyDocument(fileURL: url)
try doc.load(fromContents: data, ofType: nil)
self.lab.text = doc.string
handler(nil)
} catch {
handler(error)
}
}
Again, whether that's legal, I have no idea, but I feel better about it than calling read directly, because at least I'm passing through a file coordinator.
When I'm loading video from iCloud, I'd want a UIProgressView to be updated about the progress. For some reason, progressHandler doesn't get called. I've tried to do it like it's done in SamplePhotosApp (which loads images though), but can't get it working.
Here's the code I use for options and requesting the video:
let videoRequestOptions = PHVideoRequestOptions()
videoRequestOptions.deliveryMode = .FastFormat
videoRequestOptions.version = .Original
videoRequestOptions.networkAccessAllowed = true
videoRequestOptions.progressHandler = { (progress, error, stop, info) in
dispatch_async(dispatch_get_main_queue(), {
print(progress)
self.progressView.progress = Float(progress)
})
}
PHImageManager.defaultManager().requestPlayerItemForVideo(asset, options: videoRequestOptions, resultHandler: {
result, info in
self.videoPlayer = AVPlayer(playerItem: result!)
})
This code won't even print anything, so it looks like the progressHandler block won't be called at all. The video I'm requesting is in iCloud, because this will crash if networkAccessAllowed is set to false.
Where's the problem?
One solution is using PHImageManager's requestAVAssetForVideo instead of requestPlayerItemForVideo. After that you can create a new AVPlayerItem with the AVAsset you got as a result. This will work with the options that were used in the question.
I think this has something to do with the way videos are stored in iCloud, because they probably aren't AVPlayerItems in iCloud. This just an educated guess, and I don't have any facts about why the other method doesn't work properly.