-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Revert change to folder filtering + add special handling for shuffle arg instead (fixes regression: #4548) #4566
Conversation
956a789
to
8d7c710
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment in AppDelegate
. Shuffling is happening after the first file has started playing, so the first file played in a directory is always the same.
iina/AppDelegate.swift
Outdated
@@ -330,6 +330,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate { | |||
} | |||
|
|||
if let pc = lastPlayerCore { | |||
if commandLineStatus.shufflePlaylist { | |||
pc.toggleShuffle() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this does not work. Apparently it is being executed too late. Running this over and over again:
iina-cli --mpv-shuffle ~/Movies/
Starts playing the exact same file each time. The same file that is played without the --mpv-shuffle
option. IINA needs to load the playlist, send the shuffle command and then start playing the first file in the playlist. Something like that so the first file played is random. From the behavior I expect the first file is selected before the playlist has been shuffled.
I'm working on a solution, but this is turning out to be a tricky fix. It's going to involve manipulating the file opening process so that the user doesn't see the initial load before playlist shuffle. But the loading process has a few asynchronous stages, and I need to make sure I have a very good understanding of how they all work in order to feel confident about the fix. Getting minor surgery on my wrist & fingers later today. Will be mostly away from keyboards for a couple of weeks. If a fix is urgently needed it may be better to just back out the changes from #4439 until this is ready. |
@svobs Still busy, or have time to work on this one? We are currently actively reviewing and merging fixes for regressions, so this one is a high priority to fix. The problem with the current fix is that I added a flag to var shufflePending = false And a new listener: func onLoad() {
guard shufflePending else { return }
Logger.log("Shuffling playlist")
mpv.command(.playlistShuffle)
mpv.command(.playlistPlayIndex, args: ["0"])
shufflePending = false
} I then added this to static let onBeforeStartFile = MPVHook("on_before_start_file") And then added this to addHook(MPVHook.onBeforeStartFile, hook: MPVHookValue(withBlock: { next in
self.player.onLoad()
next()
})) And changed the code in This uses the on_before_start_file hook:
More testing is needed. |
Less busy now, but still going to be a busy month. @low-batt as mentioned before, it'd be easier to just revert. Adding support for CLI shuffle looks tricky relative to the amount of reward, and if the goal is just to have better compatibility with mpv I'm sure there are lower-hanging fruit to pick. But I'm guessing it's a feature you use regularly? ;) Well, you did somehow get the fix for "exact" seek pushed through and I'm grateful for that, so I'll work on this a bit more. Never used mpv hooks until now, but will try that. Hopefully it can do a shuffle before the file is marked as being "open"... |
We can't just revert as that would reintroduce issue #4434. Using A hook is used to make this happen as soon as possible. I think this works, but more testing is needed. It looks like If you need any help adding the hook, I can add that code to sources and post them here. Thanks very much for the |
8d7c710
to
af8c636
Compare
@low-batt I implemented the changes as you outlined, but found that So I changed it to use the And it looks like this works. I confirmed in my testing that all items in my test playlist were shuffled and then the first item in the shuffled playlist was loaded, and that IINA's playback history and Recent Documents lists both correctly indicate that only that one file was added to each. |
d404489
to
8417e67
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few minor things that could be cleaned up, but changes look good to me.
Glad you caught the problem and switched to on_load
.
I see the PR has already been updated. Some of my comments have already been addressed. One minor one remains. Not critical to fix |
Made another commit with some cleanup. @low-batt just saw your review comments - sorry! I did some further testing, and this time dug in to the relevant mpv code. I can now be confident saying that the |
…ept mpv shuffle arg & convert it to playlist-shuffle command. Fixes regression where folder filtering was broken, while still supporting shuffle via cmd-line
8417e67
to
66ef1b2
Compare
Agreed, we have to use the hook and |
One last change I missed in my review. The declaration of the @Atomic var shufflePending = false The flag is set by the main thread and then read by the thread processing Sorry I missed this. |
A lock isn't necessary here due to the context in which it's used. Remember that
|
This is the race condition:
From Wikipedia Multithreaded programming and memory visibility:
The lock provides the memory fence that forces the write by the main thread out of the CPU core's cache and into main memory. Due to the simplistic use in this case a memory barrior could be used instead of a lock. Pretty sure IINA does not have any examples of that. In this case it is likely the write will always have been flushed to main memory. But it is good practice to lock everything that is shared between threads rather than trying to identify cases where it is safe to run naked. How is the playlist being loaded? Is IINA doing that in a background thread? Is that properly coordinated? Caught me about to look at that. |
In // open the first file
open(playableFiles[0])
// add the remaining to playlist
playableFiles[1..<count].forEach { url in
addToPlaylist(url.isFileURL ? url.path : url.absoluteString)
} Seems like it is IINA that is causing the first file to start playing. Maybe the hook can be avoided by changing this code to check |
The "race condition" you described sounds more like the possibility of reading stale data due to thread-local caching. I know in Java this is the reason for the existence of the On the other hand, the playlist loading is a problem which looks like a real race condition. I had forgotten that it was being assembled in IINA now and not just submitting to mpv...
Unfortunately no, because the call to |
I was worried that changing this code might prove to be "interesting"… It was the description of the problem that caused the switch to On the "atomic" issue (which may not be applicable to this PR anymore)… These days there are multiple issues with respects to sharing data between threads. The compiler may reorder operations. The hardware also may reorder operations. And there is the issue of on CPU core caches and when values are written to and read from main memory. Using a lock addresses these issues. On the existing flags. There are lots of threading problems. If you enable TSAN you will see problems detected. As the planned fix is disruptive I am holding off until the 1.4.0 Beta. But since this is new code I didn't want us to introduce any new problems. |
I didn't read the whole thread but Swift uses C++'s memory model if that helps |
Actually doing a quick test using // This is called on the main thread
func openURLs(_ urls: [URL], shouldAutoLoad autoLoad: Bool = true) -> Int? {
// ...
// open the first file
open(playableFiles[0])
// add the remaining to playlist
var index: Int = 1
playableFiles[1..<count].forEach { url in
Logger.log("Adding URL \(index) of \(count)")
addToPlaylist(url.isFileURL ? url.path : url.absoluteString)
index += 1
Thread.sleep(forTimeInterval: 1.0)
}
Logger.log("Done adding URLs to playlist")
// ... Then I realized that the hook itself has a blocking mechanism built-in! It will wait until the hook sends So I made a simple change, so that the hook puts sends work to the main queue. This way, addHook(MPVHook.onLoad, hook: MPVHookValue(withBlock: { [self] next in
DispatchQueue.main.async { [self] in
Logger.log("Processing mpv 'on_load' hook", level: .verbose, subsystem: player.subsystem)
player.fileWillLoad()
next() // <-- calls mpv_hook_continue()
}
})) |
Yes, a hook is like a synchronous event that blocks the player. I played with not opening first. I didn't encounter any crashes. But the playlist was not populated. Not sure what was up with that. So I didn't find an alternative to using the hook. The primary goal of the next release is to fix regressions. So we are trying to merge changes that are safe and won't introduce any new problems. This fix is making me nervous. The latest change brings a 3rd thread into the picture. If the main thread attempts to access If we wanted to be more conservative we could move the check of As we now know it was the thread race condition that caused the problem should we go back to using the |
You're right...that is technically a better place to do it. I've updated the code accordingly.
Not sure what you mean. Are you talking about: the main thread, the MPVController queue, and libmpv's runtime thread? Those were in active use already. This doesn't change things in that regard.
It's already accessing mpv and not deadlocking. When the main thread calls Also recall that to deadlock, you need two threads depending on each other at the same time; you're safe if you always block in the same direction. We have a few places during startup, such as
I personally like the cleaner, simpler design of the way it is now, and feels like the the more "Apple" way. If it causes any increased delay in the loading process due to the |
I should have explained more. We know Previously the hook ran on the With the change to queue the work to the main thread there is a tiny window where other operations could run on the main thread before the hook task. What could that be? Would something run an Do I think that is a high possibility? No. But the point is that we moved from a very deterministic code path where we could say exactly what would happen to one that opened the possibility that other operations might run in the middle of this critical operation. To test this I added a 5 second delay before the queuing to the main thread and then tried clicking on menu items. IINA did not deadlock. It terminated with a fatal error due to
Thanks to the use of a generic error we don't know what this is about. My guess is that this is an existing problem that is normally not encountered due to the small window of vulnerability during the initial loading of a video. But that is just a guess. I'm not sure how I can test that on a version of IINA without these changes. Anyway, moving the flag before queuing to the main thread would mean the risk of such a problem is restricted to use of the Do I think we need to do that? I'm thinking no. I'm pretty confident we won't have a problem. But then I didn't expect IINA would fail with a fatal error during the test I ran. |
Can you share your test code? I'm very curious about how this happened. I have on rare occasion seen a -12 error from mpv when I was resuming playback. Although likely that your scenario is unrelated, I'm like to get more insight into how that could happen. I do understand the spirit of your objection though, and though I'm not yet sold about the need for the lock around the boolean, I went ahead and added it. I also went a little bit further and refactored the code so that it doesn't add a hook at all unless it needs to, which should keep things quite pristine for those who never use CLI shuffle. |
I didn't save my test code because it is trivial to create it. I merely added this line: Thread.sleep(forTimeInterval: 5.0) Before the call in I also changed the default for The delay means I have 5 seconds to invoke some other operation while the hook is running. I was trying various stuff under the |
…er check whether it is still needed. Add some comments, cleanup
0b5d435
to
d713299
Compare
Changes look good. On booleans that are shared between threads… If this was C++ we'd be using The correct way to code this today is to put properties that are shared between threads into an Actor. However Swift concurrency features use runtimes added in macOS 10.15. So IINA can not use these language features. Another choice would be to use swift-atomics. I asked the team about this, but they did not want to establish a dependency on this package. That is why at this time we are using our own lock code. |
Description:
shuffle yes
(or equivalent) from command-line & converts it toplaylist-shuffle
after playlist is populated.