Skip to content

Create JavascriptPlayerProxy#14314

Merged
JoergAtGithub merged 3 commits into
mixxxdj:mainfrom
christophehenry:JavascriptPlayerProxy
Jun 20, 2025
Merged

Create JavascriptPlayerProxy#14314
JoergAtGithub merged 3 commits into
mixxxdj:mainfrom
christophehenry:JavascriptPlayerProxy

Conversation

@christophehenry
Copy link
Copy Markdown
Contributor

@christophehenry christophehenry commented Feb 9, 2025

Continuation of #14175.

cr7pt0gr4ph7

This comment was marked as outdated.

@ronso0
Copy link
Copy Markdown
Member

ronso0 commented Feb 10, 2025

@cr7pt0gr4ph7
Copy link
Copy Markdown
Contributor

FYI the corresponding Zulip convo is https://mixxx.zulipchat.com/#narrow/channel/109171-development/topic/Expose.20track.20informations.20to.20controllers

Thanks, I missed that! Still getting used to how the communication is spread across the different channels.

@Swiftb0y
Copy link
Copy Markdown
Member

The final API might look totally different. For example, based on the current design pattern of the Engine JS API, the following design could make sense:

Thanks for chirping in @cr7pt0gr4ph7. I appreciate your suggestion but IMO it has also been discussed previously and rejected (See the corresponding zulip thread). The engine design is plagued of bad legacy decisions (decisions that needed to be made back then because either the current mixxx devs didn't know any better or Qt didn't give them any better choice). This is supposed to signify a break and a potential alternative to overcome that. engine.getString just continues that bad design legacy.

@cr7pt0gr4ph7
Copy link
Copy Markdown
Contributor

cr7pt0gr4ph7 commented Feb 11, 2025

Thanks for chirping in @cr7pt0gr4ph7. I appreciate your suggestion but IMO it has also been discussed previously and rejected (See the corresponding zulip thread).

I wasn't aware of the Zulip discussion when I wrote that comment. I've hidden my comment and added a note at the top to avoid future confusion, sorry for the noise. I'll continue the discussion w.r.t. API design over at Zulip.

@cr7pt0gr4ph7
Copy link
Copy Markdown
Contributor

Additionally, I'd also like to make the proposal of preactively linking Zulip discussions in GitHub issues and PRs for future cross-reference when these contain highly relevant discussion, such as here ;)

Copy link
Copy Markdown
Member

@acolombier acolombier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking pretty much good to go IMO!
Only thing is that it would be worth to combining our effort with #14516, which break the dependencies between the player and a track (quick TL;DR on why this is needed - QML needs a way to interact with track that aren't loaded on player). Note that this change also removes all the waveform left over bits, that are now irrelevant thanks to the rendergraph waveform library, and this should help simplifying your PR.

Also, it would be great if you could put typing instructions in res/controllers/engine-api.d.ts and also provide a small example on how this is intended to be used. Also, feel free to implement a basic usage in a controller mapping you feel relevant just to help with understanding and testing (just make sure to use a separate commit for it for better change control)

Comment thread src/qml/qmlplayerproxy.cpp Outdated
@christophehenry
Copy link
Copy Markdown
Contributor Author

@acolombier Since you're moving most of QmlPlayerProxy's code into QmlTrackProxyin #14516, would you agree to create the read-only JavascriptTrackProxy?

@JoergAtGithub
Copy link
Copy Markdown
Member

The Deck/Player might be better parent object here than the track, as it could provide additional information that depend on playposition or rate, like the formated actual key string, which is often used in jogwheel displays.

@christophehenry
Copy link
Copy Markdown
Contributor Author

Ok so we're back to defining a wrapper around QmlTrackProxy+QmlPlayerProxy then?

@acolombier
Copy link
Copy Markdown
Member

The Deck/Player might be better parent object here than the track, as it could provide additional information that depend on playposition or rate

The idea with the split is to allow both. Player (which can be a deck, sampler or preview) will have playing information (rate, position, ...) as well current loaded track, which itself will have its own information, unrelated to player.

@JoergAtGithub
Copy link
Copy Markdown
Member

Ok so we're back to defining a wrapper around QmlTrackProxy+QmlPlayerProxy then?

This code is fine, it just needs to be adjusted as described by @acolombier above!

@christophehenry
Copy link
Copy Markdown
Contributor Author

Well @acolombier's PR splits the code in two classes so I don't really understand how this code fits.

@christophehenry christophehenry force-pushed the JavascriptPlayerProxy branch from ca4c97c to b7ee0c3 Compare May 23, 2025 07:06
@christophehenry
Copy link
Copy Markdown
Contributor Author

I rebased this PR and added the implementation.

// Don't set a parent here, so that the QML engine deletes the object when
// the corresponding JS object is garbage collected.
JavascriptPlayerProxy* pPlayerProxy = new JavascriptPlayerProxy(
s_pPlayerManager->getPlayer(deck), nullptr);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlayerManager::getPlayer will return a null ptr if deck doesn't exist, could you please handle that gracefully and return a QJSEngine::newErrorObject?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi, this a new pattern where our API would return an error. Usually it just returns undefined (which would be the case with a nullptr). Since we don't really return different errors at the moment, I would prefer to stick with the consistency of undefined. Wdyt?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy with the middle ground of returning null if we think this is best. I think we shouldn't return an invalid JavascriptPlayerProxy tho

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I changed that in the last commit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I resolve this so it's clearer for me what's left?

Comment thread src/controllers/scripting/controllerscriptenginebase.h Outdated
Comment on lines +17 to +26
Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY trackChanged)
Q_PROPERTY(QString artist READ getArtist NOTIFY artistChanged)
Q_PROPERTY(QString title READ getTitle NOTIFY titleChanged)
Q_PROPERTY(QString album READ getAlbum NOTIFY albumChanged)
Q_PROPERTY(QString albumArtist READ getAlbumArtist NOTIFY albumArtistChanged)
Q_PROPERTY(QString genre READ getGenre STORED false NOTIFY genreChanged)
Q_PROPERTY(QString composer READ getComposer NOTIFY composerChanged)
Q_PROPERTY(QString grouping READ getGrouping NOTIFY groupingChanged)
Q_PROPERTY(QString year READ getYear NOTIFY yearChanged)
Q_PROPERTY(QString trackNumber READ getTrackNumber NOTIFY trackNumberChanged)
Q_PROPERTY(QString trackTotal READ getTrackTotal NOTIFY trackTotalChanged)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it is necessary to implement NOTIFY here. Since this code won't be used by QML (as it has its own implementation), there is no need to provide this to support property binding. This will allow you to remove most of the signals and reduce the code duplication with src/qml/qmltrackproxy.h

Copy link
Copy Markdown
Contributor Author

@christophehenry christophehenry May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean for this property or all of them? I think NOTIFY is useful. In combination with #13935, it allows the following:

engine.getPlayer("deck1").titleChanged.connect(newTitle => {
    displayTitle(
        engine.convertCharset(engine.WellKnownCharsets.Latin1, newTitle)
    )
}) 

I could even add a convenience function to ComponentsJS

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, we should def also use signals from JS. Otherwise we'll need yet another makeConnection like facility or people will abuse (likely only tangentially related) COs instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets split any ComponentsJS changes to another PR though

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, I didn't see that this will actively be supported in the script. This is looking good then. The duplication remains a bit of a bummer IMO, but don't want to block this further so if this isn't a problem for other member, happy to ignore!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I resolve this so it's clearer for me what's left?

Comment thread src/controllers/scripting/controllerscriptenginebase.cpp
Comment thread src/controllers/scripting/javascriptplayerproxy.cpp Outdated
Comment thread src/controllers/scripting/javascriptplayerproxy.cpp Outdated
Comment thread src/controllers/scripting/javascriptplayerproxy.cpp Outdated
@christophehenry christophehenry force-pushed the JavascriptPlayerProxy branch 2 times, most recently from cabd6b9 to 217ecee Compare May 23, 2025 12:15
Comment thread res/controllers/engine-api.d.ts Outdated
// Don't set a parent here, so that the QML engine deletes the object when
// the corresponding JS object is garbage collected.
JavascriptPlayerProxy* pPlayerProxy = new JavascriptPlayerProxy(
s_pPlayerManager->getPlayer(deck), nullptr);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi, this a new pattern where our API would return an error. Usually it just returns undefined (which would be the case with a nullptr). Since we don't really return different errors at the moment, I would prefer to stick with the consistency of undefined. Wdyt?

Comment thread src/controllers/scripting/javascriptplayerproxy.h Outdated
Comment thread src/coreservices.cpp
@christophehenry christophehenry force-pushed the JavascriptPlayerProxy branch from e750320 to d960c0a Compare May 29, 2025 10:38
@JoergAtGithub
Copy link
Copy Markdown
Member

TypeScript declaration is LGTM now too! Thank you!
The test is the last missing piece now!

@Swiftb0y
Copy link
Copy Markdown
Member

friendly ping @christophehenry

@christophehenry
Copy link
Copy Markdown
Contributor Author

@Swiftb0y Did you mean to ping @JoergAtGithub? I'm waiting for their input on how to load a track to the player and test the slots.

@Swiftb0y
Copy link
Copy Markdown
Member

Ah, I see. I didn't know where we were. Then this is indeed a friendly reminder @JoergAtGithub

@christophehenry
Copy link
Copy Markdown
Contributor Author

Ah I see @JoergAtGithub opened christophehenry#3. I don't why I didn't get notified… I may be able to progress this week.

…pping_validation_test and controllerscriptenginelegacy_test executable without crash
@JoergAtGithub JoergAtGithub added this to the 2.7-beta milestone Jun 19, 2025
@christophehenry
Copy link
Copy Markdown
Contributor Author

christophehenry commented Jun 20, 2025

Alright! Tests done!

Edit: ah… I should add a test for the trackUnloaded slot too. I'll do tonight. In the meantime, don't hesitate to leave your comments!

Comment on lines +965 to +974
for (auto [key, value] : expectedValues.asKeyValueRange()) {
EXPECT_QSTRING_EQ(value, qjsvalue_cast<QString>(evaluate(QString("player.%1").arg(key))));
EXPECT_QSTRING_EQ(value, qjsvalue_cast<QString>(evaluate(QString("result.%1").arg(key))));
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a GoogleTest equivalent to Python's unittest.TestCase.subTest? Couldn't find one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. The decoding test has some code that tries to accomplish something similar, but I don't want you to reuse that. Looking at your test code however, I think you'll want to write a Value-Parameterized Test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll just go with a custom failure message.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats fine too, though a parametrized test wouldn't really be that much complicated IMO.

@christophehenry christophehenry force-pushed the JavascriptPlayerProxy branch from fbd30fb to c6f0e2d Compare June 20, 2025 17:17
Comment on lines +153 to +146
while (deck->getEngineDeck()->getEngineBuffer()->isTrackLoaded()) {
QTest::qSleep(100);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly, this doesn't work. The test just hangs. If I execute this in the debugger with breakpoints, deck->slotEjectTrack(1.0) correctly ejects the track and the test passes. So I'm not correctly waiting here. There's a concurrency problem. I'd accept any help on how to solve this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try to eject from JavaScript using engine.setValue('[Channel1]', 'eject', 1); ?

Copy link
Copy Markdown
Contributor Author

@christophehenry christophehenry Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit I didn't have that idea. Lemme try.
Edit: @JoergAtGithub hmm… This solution seems to not even eject the track. Even in the debugger, the event aren't fired :thinking:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be fine if the testcase doens't check the eject case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, deal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also processEvents() in the busy-wait loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly no, that still hangs or not even eject, depending on the solution. I tried a number of combinations around this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably need to call it twice, as in other tests:

void processEvents() {
// Calling processEvents() twice ensures that at least all queued and
// the next round of emitted events are processed.
// Test fails occurred here for a local debug build on Linux, but not on CI (see https://github.com/mixxxdj/mixxx/pull/4588)
application()->processEvents();
application()->processEvents();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I called the processEvents of the test class which already does that. But it doesn't make a difference.

@christophehenry christophehenry force-pushed the JavascriptPlayerProxy branch 2 times, most recently from 88fa23b to 9f525d5 Compare June 20, 2025 18:29
@christophehenry christophehenry force-pushed the JavascriptPlayerProxy branch from 9f525d5 to 80e959a Compare June 20, 2025 20:50
Copy link
Copy Markdown
Member

@JoergAtGithub JoergAtGithub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you for your contribution and your patience!

@JoergAtGithub JoergAtGithub merged commit 07bcb86 into mixxxdj:main Jun 20, 2025
3 checks passed
@daschuer
Copy link
Copy Markdown
Member

After merging this Mixxx crashed during shutdown: See #14982

@christophehenry christophehenry deleted the JavascriptPlayerProxy branch June 21, 2025 02:27
@Eve00000
Copy link
Copy Markdown
Contributor

Congratulations for the work and perseverance.
(Too bad for the crash on exit now)

@christophehenry
Copy link
Copy Markdown
Contributor Author

Awesome! It's the last piece I needed to propose an official DN-S3700 controller ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants