elements icon indicating copy to clipboard operation
elements copied to clipboard

Feature Request: Persist mux-player settings in LocalStorage

Open PJUllrich opened this issue 1 year ago • 7 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Which Mux Elements/Packages does this apply to? Select all that apply

mux-player, mux-player-react

Description

It would be great if the mux-player would persist its configuration across page reloads and navigations. So, if a user e.g. chooses 1.5x playback speed and no captions, this configuration should not reset when the page reloads or the user navigates to another URL on the same domain.

Expected Behavior

If a user chooses some mux-player settings, they are set automatically when the user refreshes the page or plays another video on the same domain.

PJUllrich avatar Feb 12 '24 11:02 PJUllrich

Thank you for your request.

I'll bring this up among the team along with the rest of your requests. We should be getting back to you soon.

Thanks again!

daytime-em avatar Feb 15 '24 01:02 daytime-em

Hey @PJUllrich this is definitely not a wild request but we'll need to think this through on some of the details. We should have a bit of this in place (volume and selected subtitles lang). Playback rate is definitely another "user preference" that sounds reasonable (so long as there's some way to opt-out or opt-in). This would most likely end up being a feature of Media Chrome, the UI architecture we use "under the hood" for Mux Player.

Other than playback rate and the ones already supported, are there other "user preferences" you'd be interested in seeing "remembered" across page loads?

cjpillsbury avatar Feb 16 '24 15:02 cjpillsbury

Hey @cjpillsbury thanks for the response 🙏 I think that volume, captions language, and playback speed are the most important settings. So, that'd be great to have :)

PJUllrich avatar Feb 16 '24 20:02 PJUllrich

The three big settings we get comments about are playback rate, video quality, and captions. I have hacked around the dom to support the first two, but haven't been able to figure out how to persist caption settings to localstorage. Would love for Mux to add support for this 🙏

benadam11 avatar Feb 21 '24 16:02 benadam11

@benadam11 while this is not implemented yet, would you mind sharing your workaround for playback rate and video quality? My users are asking for it almost daily :D

PJUllrich avatar Mar 14 '24 09:03 PJUllrich

@PJUllrich basically I am just selecting their media element and listening for events. I am sure this is very brittle and hacky but "it works".

function usePlaybackController({
  el,
  setPlaybackRate,
}: {
  el: HTMLMediaElement | null;
  setPlaybackRate: (value: PlaybackValue) => void;
}) {
  useEffect(() => {
    const container = el?.shadowRoot
      ?.querySelector("media-theme")
      ?.shadowRoot?.querySelector("media-controller");

    const toggle = (e: any) => {
      setPlaybackRate(e.detail);
    };

    container?.addEventListener("mediaplaybackraterequest", toggle);

    return () => {
      container?.removeEventListener("mediaplaybackraterequest", toggle);
    };
  }, [setPlaybackRate, el]);
}

benadam11 avatar Mar 14 '24 14:03 benadam11

Thank you @benadam11 . This here is the solution I ended up with. It supports mute/unmute, playback rate, video quality, and even captions but there's a bug with this where the tick isn't set next to the selected text track correctly. So, if you set the captions to "Off" and reload the page, the captions are off, but if you click on the "CC" button, you'll see that e.g. the "English" caption is selected in fact its turned off. If a user clicks on the "English" caption, it doesn't actually switch. They have to first click "off" and then "English" again. This can be confusing, but I decided its a UX issue small enough to be acceptable given that I can now persist the captions preferences of users.

This is how it looks like: CleanShot 2024-03-15 at 15 58 23@2x

This is my solution:

const SET_PLAYBACK_RATE_EVENT = "mediaplaybackraterequest";
const SET_SUBTITLES_EVENT = "mediashowsubtitlesrequest";
const DISABLE_SUBTITLES_EVENT = "mediadisablesubtitlesrequest";
const SET_QUALITY_EVENT = "mediarenditionrequest";
const SET_VOLUME_EVENT = "mediavolumerequest";
const PLAY_EVENT = "mediaplayrequest";
const MUTE_EVENT = "mediamuterequest";
const UNMUTE_EVENT = "mediaunmuterequest";

const PLAYBACK_RATE_KEY = "playbackrate";
const SUBTITLES_KEY = "subtitles";
const VIDEO_QUALITY_KEY = "videoquality";
const MUTE_KEY = "mutevideo";

export default {
  mounted() {
    this.mediaController = this.el
      .querySelector("mux-player")
      .shadowRoot?.querySelector("media-theme")
      ?.shadowRoot?.querySelector("media-controller");

    // window.mc = this.mediaController;

    if (this.mediaController) {
      this.addEventListeners();
    }
  },
  addEventListeners() {
    this.mediaController.addEventListener(SET_PLAYBACK_RATE_EVENT, (event) => {
      this.saveValue(PLAYBACK_RATE_KEY, event.detail);
    });
    this.mediaController.addEventListener(SET_SUBTITLES_EVENT, (event) => {
      this.saveValue(SUBTITLES_KEY, event.detail);
    });
    this.mediaController.addEventListener(SET_QUALITY_EVENT, (event) => {
      this.saveValue(VIDEO_QUALITY_KEY, event.detail);
    });
    this.mediaController.addEventListener(MUTE_EVENT, (_event) => {
      this.saveValue(MUTE_KEY, "true");
    });
    this.mediaController.addEventListener(UNMUTE_EVENT, (_event) => {
      this.saveValue(MUTE_KEY, "false");
    });
   // We need to listen to this event because if the video is muted and the user changes the volume
   // the video is automatically unmuted.
    this.mediaController.addEventListener(SET_VOLUME_EVENT, (event) => {
      if (event.detail > 0 && localStorage.getItem(MUTE_KEY) === "true") {
        console.log("unmute");
        this.saveValue(MUTE_KEY, "false");
      }
    });
    this.mediaController.addEventListener(PLAY_EVENT, () => {
      // console.log("playing...");
      this.setAllValues();
    });
  },
  setAllValues() {
    this.setValue(PLAYBACK_RATE_KEY, SET_PLAYBACK_RATE_EVENT);
    this.setValue(VIDEO_QUALITY_KEY, SET_QUALITY_EVENT);
    this.setMute();
    this.setSubtitles();
  },
  saveValue(key, value) {
    if (value !== null) {
      // console.log(`Saving ${key} to localStorage with ${value}.`);
      localStorage.setItem(key, value);
    }
  },
  setValue(key, event_name) {
    const value = localStorage.getItem(key);
    if (value !== null) {
      // console.log(`Setting ${key} to ${value} with ${event_name}.`);
      this.sendEvent(event_name, value);
    }
  },
  setSubtitles() {
    const value = localStorage.getItem(SUBTITLES_KEY);
    if (value) {
      // The trick here is to first disable all current subtitles and then set the persisted one
      // If you don't disable the subtitles first, changing them via an Event won't have an effect.
      this.disableCurrentSubtitle();
      this.sendEvent(SET_SUBTITLES_EVENT, value);
    }
  },
  setMute() {
    const value = localStorage.getItem(MUTE_KEY);
    if (value === "true") {
      this.sendEvent(MUTE_EVENT, "true");
    }
  },
  disableCurrentSubtitle() {
    const value = this.mediaController.getAttribute("mediaSubtitlesShowing");

    if (value) {
      const elements = value.split(":");
      const subtitles = [
        {
          kind: "subtitles",
          language: elements[1],
          label: elements[2],
        },
      ];
      this.sendEvent(DISABLE_SUBTITLES_EVENT, subtitles);
    }
  },
  sendEvent(event_name, value) {
    // console.log(`Sending event ${event_name} with ${value}`);
    const event = new CustomEvent(event_name, {
      detail: value,
      composed: true,
      bubbles: true,
    });
    this.mediaController.dispatchEvent(event);
  },
};

PJUllrich avatar Mar 15 '24 15:03 PJUllrich