Jump to content

Emby Party - A watch party solution for the Emby web client


Recommended Posts

Protected
Posted (edited)

Not a question for me I'm afraid. It would require a way for plugins to load actual client-side modules across the board. I believe the problem with such a system is that many devices have very limited resources, limited support for only a subset of javascript (often very old) or both. Something that works fine in your web browser could (though won't *necessarily*) make your other devices fail in fun unexpected ways, or simply degrade your experience. I imagine those plugins would then be difficult to test and approve. And it's difficult for most of us to test on every device too...

That said, you should still be able to use a web client instance as a controller (that joins the party) and use that to remote control your native app. You won't be able to use the sidebar but it should keep sync with the party.

Edited by Protected
podonnell
Posted
21 hours ago, Protected said:

Not a question for me I'm afraid. It would require a way for plugins to load actual client-side modules across the board. I believe the problem with such a system is that many devices have very limited resources, limited support for only a subset of javascript (often very old) or both. Something that works fine in your web browser could (though won't *necessarily*) make your other devices fail in fun unexpected ways, or simply degrade your experience. I imagine those plugins would then be difficult to test and approve. And it's difficult for most of us to test on every device too...

That said, you should still be able to use a web client instance as a controller (that joins the party) and use that to remote control your native app. You won't be able to use the sidebar but it should keep sync with the party.

Yeah, totally understood. Emby developers would need to adapt this for use inside the Windows client, and any other native apps on TVs and entertainment devices.

I do like the idea of connecting via web, but controlling a remote viewing device. However I think it might be too big of an ask for my users. I think I'll advise the web client anyway, and just deal with any transcodes that occur.

Very much hoping some type of official implementation at some point though.

Posted
On 5/6/2025 at 7:07 PM, Protected said:

It would require a way for plugins to load actual client-side modules across the board

Hi. Just to set expectations, I'm afraid there is no way we could ever realistically make that happen - at least not with any app that is delivered via a third party store - which, I think is pretty much all of them now.

Protected
Posted

For TOS-related reasons?

Posted
2 hours ago, Protected said:

For TOS-related reasons?

For a multitude of reasons.  We'd have absolutely no control over what those modules did.  The potential security and privacy implications would be huge - not to mention the liability through the app stores.

evil_crab
Posted

Any updates on when/if official watch-together functionality is coming?

lexisdude
Posted (edited)
On 5/8/2025 at 1:37 PM, ebr said:

For a multitude of reasons.  We'd have absolutely no control over what those modules did.  The potential security and privacy implications would be huge - not to mention the liability through the app stores.

Ok, so that I understand. Are you saying that creating a group "chat" , where multi-persons are sharing a stream together, would not be possible - or could somehow violate the TOS on third party apps? I'm not bashing or being mean, I just want to understand. Emby in essence is a server, server typically means providing access to multiple people/accounts on the server. MUDS were the foundation to multiplayer online games. How is Co-sharing a video stream with multiple people differ? I would think it would only be the matter of allowing a grouping of people to a single transcode and starting them at the current time the transcode is at. Or am I thinking wrong here

Edited by lexisdude
Protected
Posted

As I understand it, the issue aren't watch parties themselves, but specifically the ability for a plugin developer (who can be anyone) to deploy code on an application distributed through a device's app store (to your smart device, the Emby server is an external device). I'll reserve my comments on whether "smart devices" should be so controlling of their platforms!

Implicit in ebr's comment is that the feature would technically be OK for clients not distributed through an app store (technologically this is possible for Android too), but I imagine it's not going to be a priority if it's wasting development time for something only a subset of users gets to use and which creates a rift between the experiences of users on different platforms...

Posted

Correct, it isn't this functionality specifically.  It was the request to allow a plugin to inject code into an app in general that we would not be able to allow.

podonnell
Posted

So a natively developer functionality would be possible though? Has there ever been a comment on if something like this is planned?

Posted
17 hours ago, podonnell said:

Has there ever been a comment on if something like this is planned?

It is on our "interested in" list.  Incidentally, I believe Plex has removed this feature now.

 

  • 3 months later...
Hijack_in_Indy
Posted
On 1/9/2025 at 1:11 AM, KayDox64 said:

Do you think we can get a video tutorial for how this should install? the instructions dont show what it looks like, for example my appheader.js was minified, and there was no position 21877, at least not one i could find.

I'd love some addition info as well on inserting the code into the 4.8.11 appheader.js file.  Position 21877 doesn't seem to line up in that version of the file with the install instructions.  

 

position.png

Posted (edited)

What you're trying to do is to locate the `render` function, then insert the code at the end of this function, before the closing curly brace. Keep an eye out for how the contents are minified and whether there's a return statement at the end of the function, which might affect whether you need to end the previous line with a comma or semicolon (and code after the return statement will not run). 

The code:

Emby.importModule("./modules/embyparty/partyheader.js").then(function(PartyHeader) { return (new PartyHeader()).show(skinHeaderElement.querySelector(".headerRight"))

Is loading the party header module and then attaching it to the HTMLElement whose reference is passed to the show() function. You can pass it something else, which will affect where the party button appears, and you can also technically load it from somewhere else.

Here's a more readable syntax explanation with an arrow function:

Emby.importModule(PATH_TO_MODULE).then(moduleRef => moduleRef.show(REFERENCE_TO_HTML_ELEMENT))

Please note that I am not currently immediately able to update this project so I can't guarantee it's working with newer versions!

Edited by Protected
Hijack_in_Indy
Posted

Thanks.  I put the file contents through beautifier.io just so I could find the complete render function.  Below is a more readable block of that function.

    function render(instance) {
        var Back, Home, Menu, Help;
        instance.element = skinHeaderElement, headerLeft = skinHeaderElement.querySelector(".headerLeft"), Menu = _globalize.default.translate("Menu"), Home = _globalize.default.translate("Home"), Back = _globalize.default.translate("Back"), Help = _globalize.default.translate("Help"), headerLeft.innerHTML = '\n            <button type="button" is="paper-icon-button-light" class="headerBackButton headerButton headerSectionItem hide-mouse-idle-tv hide" tabindex="-1" title="'.concat(Back, '" aria-label="').concat(Back, '">\n                <i class="md-icon autortl">&#xe2ea;</i>\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerHomeButton headerButton headerSectionItem hide-mouse-idle-tv hide md-icon md-icon-fill" tabindex="-1" title="').concat(Home, '" aria-label="').concat(Home, '">\n                &#xe88a;\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerMenuButton headerButton headerSectionItem hide md-icon" title="').concat(Menu, '" aria-label="').concat(Menu, '">\n                &#xe5D2;\n            </button>\n            <h2 class="pageTitle headerSectionItem">&nbsp;</h2>\n\n            <a type="button" is="emby-linkbutton" class="paper-icon-button-light headerHelpButton dialogHeaderButton button-help headerButton headerSectionItem hide secondaryText" title="').concat(Help, '" aria-label="').concat(Help, '" target="_blank" href="#">\n                <i class="md-icon autortl-arabic">&#xe887;</i>\n            </a>\n        '), Back = _globalize.default.translate("ManageEmbyServer"), Home = _globalize.default.translate("Settings"), Menu = _globalize.default.translate("Search"), Help = _globalize.default.translate("PlayOnAnotherDevice"), skinHeaderElement.querySelector(".headerRight").innerHTML = '\n            <div class="headerSelectedPlayer headerSectionItem hide">\n\n            </div>\n            <button type="button" is="paper-icon-button-light" class="headerCastButton headerButton hide headerSectionItem md-icon hide" title="'.concat(Help, '" aria-label="').concat(Help, '">\n                &#xe307;\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerSearchButton headerButton hide headerSectionItem md-icon hide" title="').concat(Menu, '" aria-label="').concat(Menu, '">\n                &#xe8B6;\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerUserButton headerButton headerSectionItem hide" title="').concat(Home, '" aria-label="').concat(Home, '">\n                <i class="md-icon largeIcon">&#xe7FD;</i>\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerSettingsButton headerButton headerSectionItem md-icon hide" title="').concat(Back, '" aria-label="').concat(Back, '">\n                &#xe8B8;\n            </button>\n            <div class="headerClock headerSectionItem hide"></div>\n        '),
            function(instance) {
                var parent = instance.element;
                headerBackButton = parent.querySelector(".headerBackButton"), headerHomeButton = parent.querySelector(".headerHomeButton"), headerMenuButton = parent.querySelector(".headerMenuButton"), headerCastButton = parent.querySelector(".headerCastButton"), headerHelpButton = parent.querySelector(".headerHelpButton"), headerSearchButton = parent.querySelector(".headerSearchButton"), selectedPlayerText = parent.querySelector(".headerSelectedPlayer"), headerRight = parent.querySelector(".headerRight"), headerBackButton.addEventListener("click", onBackClick), headerHomeButton.addEventListener("click", onHomeClick), headerSearchButton.addEventListener("click", onSearchClick), headerCastButton.addEventListener("click", onCastButtonClick), parent.querySelector(".headerUserButton").addEventListener("click", onUserButtonClick), parent.querySelector(".headerSettingsButton").addEventListener("click", onSettingsButtonClick), headerMenuButton.addEventListener("click", onHeaderMenuButtonClick), boundLayoutModeChangeFn = onLayoutModeChange.bind(instance), _events.default.on(_layoutmanager.default, "modechange", boundLayoutModeChangeFn), _events.default.on(_playbackmanager.default, "playerchange", updateCastIcon), _events.default.on(_playbackmanager.default, "playqueuestart", onNewPlayQueueStart), _events.default.on(_connectionmanager.default, "localusersignedin", onLocalUserSignedIn), _events.default.on(_connectionmanager.default, "localusersignedout", onLocalUserSignedOut), _events.default.on(_api.default, "UserUpdated", onUserUpdated), document.addEventListener("viewbeforeshow", onViewBeforeShow.bind(instance)), document.addEventListener("viewshow", onViewShow.bind(instance)), _inputmanager.default.on(skinHeaderElement, onHeaderCommand), instance.pageTitleElement = parent.querySelector(".pageTitle"), resetPremiereButton(), _events.default.on(_connectionmanager.default, "resetregistrationinfo", resetPremiereButton)
            }(instance), setRemoteControlVisibility(), onLayoutModeChange.call(instance), _events.default.on(_navdrawer.default, "drawer-state-change", onNavDrawerStateChange), _events.default.on(_navdrawercontent.default, "dynamic-title", function(e, title) {
                this.setTitle(title)
            }.bind(instance))
    }

Just tack on insert code to make the function read like this?

    function render(instance) {
        var Back, Home, Menu, Help;
        instance.element = skinHeaderElement, headerLeft = skinHeaderElement.querySelector(".headerLeft"), Menu = _globalize.default.translate("Menu"), Home = _globalize.default.translate("Home"), Back = _globalize.default.translate("Back"), Help = _globalize.default.translate("Help"), headerLeft.innerHTML = '\n            <button type="button" is="paper-icon-button-light" class="headerBackButton headerButton headerSectionItem hide-mouse-idle-tv hide" tabindex="-1" title="'.concat(Back, '" aria-label="').concat(Back, '">\n                <i class="md-icon autortl">&#xe2ea;</i>\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerHomeButton headerButton headerSectionItem hide-mouse-idle-tv hide md-icon md-icon-fill" tabindex="-1" title="').concat(Home, '" aria-label="').concat(Home, '">\n                &#xe88a;\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerMenuButton headerButton headerSectionItem hide md-icon" title="').concat(Menu, '" aria-label="').concat(Menu, '">\n                &#xe5D2;\n            </button>\n            <h2 class="pageTitle headerSectionItem">&nbsp;</h2>\n\n            <a type="button" is="emby-linkbutton" class="paper-icon-button-light headerHelpButton dialogHeaderButton button-help headerButton headerSectionItem hide secondaryText" title="').concat(Help, '" aria-label="').concat(Help, '" target="_blank" href="#">\n                <i class="md-icon autortl-arabic">&#xe887;</i>\n            </a>\n        '), Back = _globalize.default.translate("ManageEmbyServer"), Home = _globalize.default.translate("Settings"), Menu = _globalize.default.translate("Search"), Help = _globalize.default.translate("PlayOnAnotherDevice"), skinHeaderElement.querySelector(".headerRight").innerHTML = '\n            <div class="headerSelectedPlayer headerSectionItem hide">\n\n            </div>\n            <button type="button" is="paper-icon-button-light" class="headerCastButton headerButton hide headerSectionItem md-icon hide" title="'.concat(Help, '" aria-label="').concat(Help, '">\n                &#xe307;\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerSearchButton headerButton hide headerSectionItem md-icon hide" title="').concat(Menu, '" aria-label="').concat(Menu, '">\n                &#xe8B6;\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerUserButton headerButton headerSectionItem hide" title="').concat(Home, '" aria-label="').concat(Home, '">\n                <i class="md-icon largeIcon">&#xe7FD;</i>\n            </button>\n            <button type="button" is="paper-icon-button-light" class="headerSettingsButton headerButton headerSectionItem md-icon hide" title="').concat(Back, '" aria-label="').concat(Back, '">\n                &#xe8B8;\n            </button>\n            <div class="headerClock headerSectionItem hide"></div>\n        '),
            function(instance) {
                var parent = instance.element;
                headerBackButton = parent.querySelector(".headerBackButton"), headerHomeButton = parent.querySelector(".headerHomeButton"), headerMenuButton = parent.querySelector(".headerMenuButton"), headerCastButton = parent.querySelector(".headerCastButton"), headerHelpButton = parent.querySelector(".headerHelpButton"), headerSearchButton = parent.querySelector(".headerSearchButton"), selectedPlayerText = parent.querySelector(".headerSelectedPlayer"), headerRight = parent.querySelector(".headerRight"), headerBackButton.addEventListener("click", onBackClick), headerHomeButton.addEventListener("click", onHomeClick), headerSearchButton.addEventListener("click", onSearchClick), headerCastButton.addEventListener("click", onCastButtonClick), parent.querySelector(".headerUserButton").addEventListener("click", onUserButtonClick), parent.querySelector(".headerSettingsButton").addEventListener("click", onSettingsButtonClick), headerMenuButton.addEventListener("click", onHeaderMenuButtonClick), boundLayoutModeChangeFn = onLayoutModeChange.bind(instance), _events.default.on(_layoutmanager.default, "modechange", boundLayoutModeChangeFn), _events.default.on(_playbackmanager.default, "playerchange", updateCastIcon), _events.default.on(_playbackmanager.default, "playqueuestart", onNewPlayQueueStart), _events.default.on(_connectionmanager.default, "localusersignedin", onLocalUserSignedIn), _events.default.on(_connectionmanager.default, "localusersignedout", onLocalUserSignedOut), _events.default.on(_api.default, "UserUpdated", onUserUpdated), document.addEventListener("viewbeforeshow", onViewBeforeShow.bind(instance)), document.addEventListener("viewshow", onViewShow.bind(instance)), _inputmanager.default.on(skinHeaderElement, onHeaderCommand), instance.pageTitleElement = parent.querySelector(".pageTitle"), resetPremiereButton(), _events.default.on(_connectionmanager.default, "resetregistrationinfo", resetPremiereButton)
            }(instance), setRemoteControlVisibility(), onLayoutModeChange.call(instance), _events.default.on(_navdrawer.default, "drawer-state-change", onNavDrawerStateChange), _events.default.on(_navdrawercontent.default, "dynamic-title", function(e, title) {
                this.setTitle(title)
            }.bind(instance))
    , Emby.importModule("./modules/embyparty/partyheader.js").then(function(PartyHeader) { return (new PartyHeader()).show(skinHeaderElement.querySelector(".headerRight")); });}

Thanks for monitoring this plugin thread even if you're not actively developing any more.  I hope the plugin does still work.  I'll let you know once I get it all set up right.

 

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...