Code Monkey home page Code Monkey logo

Comments (22)

jumoog avatar jumoog commented on August 12, 2024 2

It works. I am now testing with an LG TV. Enter is ignored for 5 seconds. No pause.

let introSkipper = {
    allowEnter: true,
    skipSegments: {},
    videoPlayer: {},
    // .bind() is used here to prevent illegal invocation errors
    originalFetch: window.fetch.bind(window),
};
introSkipper.d = function (msg) {
    console.debug("[intro skipper] ", msg);
  }
  /** Setup event listeners */
  introSkipper.setup = function () {
    document.addEventListener("viewshow", introSkipper.viewShow);
    window.fetch = introSkipper.fetchWrapper;
    introSkipper.d("Registered hooks");
  }
  /** Wrapper around fetch() that retrieves skip segments for the currently playing item. */
  introSkipper.fetchWrapper = async function (...args) {
    // Based on JellyScrub's trickplay.js
    let [resource, options] = args;
    let response = await introSkipper.originalFetch(resource, options);
    // Bail early if this isn't a playback info URL
    try {
      let path = new URL(resource).pathname;
      if (!path.includes("/PlaybackInfo")) { return response; }
      introSkipper.d("Retrieving skip segments from URL");
      introSkipper.d(path);
      
      // Check for context root and set id accordingly
      let path_arr = path.split("/");
      let id = "";
      if (path_arr[1] == "Items") {
        id = path_arr[2];
      } else {
        id = path_arr[3];
      }

      introSkipper.skipSegments = await introSkipper.secureFetch(`Episode/${id}/IntroSkipperSegments`);
      introSkipper.d("Successfully retrieved skip segments");
      introSkipper.d(introSkipper.skipSegments);
    }
    catch (e) {
      console.error("Unable to get skip segments from", resource, e);
    }
    return response;
  }
  /**
  * Event handler that runs whenever the current view changes.
  * Used to detect the start of video playback.
  */
  introSkipper.viewShow = function () {
    const location = window.location.hash;
    introSkipper.d("Location changed to " + location);
    if (location !== "#/video") {
      introSkipper.d("Ignoring location change");
      return;
    }
    introSkipper.injectCss();
    introSkipper.injectButton();
    document.body.addEventListener('keydown', introSkipper.eventHandler, true);
    introSkipper.videoPlayer = document.querySelector("video");
    if (introSkipper.videoPlayer != null) {
      introSkipper.d("Hooking video timeupdate");
      introSkipper.videoPlayer.addEventListener("timeupdate", introSkipper.videoPositionChanged);
    }
  }
  /**
  * Injects the CSS used by the skip intro button.
  * Calling this function is a no-op if the CSS has already been injected.
  */
  introSkipper.injectCss = function () {
    if (introSkipper.testElement("style#introSkipperCss")) {
      introSkipper.d("CSS already added");
      return;
    }
    introSkipper.d("Adding CSS");
    let styleElement = document.createElement("style");
    styleElement.id = "introSkipperCss";
    styleElement.innerText = `
    @media (hover:hover) and (pointer:fine) {
        #skipIntro .paper-icon-button-light:hover:not(:disabled) {
            color: black !important;
            background-color: rgba(47, 93, 98, 0) !important;
        }
    }
    #skipIntro .paper-icon-button-light.show-focus:focus {
        transform: scale(1.04) !important;
    }
    #skipIntro.upNextContainer {
        width: unset;
    }
    #skipIntro {
        padding: 0 1px;
        position: absolute;
        right: 10em;
        bottom: 9em;
        background-color: rgba(25, 25, 25, 0.66);
        border: 1px solid;
        border-radius: 0px;
        display: inline-block;
        cursor: pointer;
        opacity: 0;
        box-shadow: inset 0 0 0 0 #f9f9f9;
        -webkit-transition: ease-out 0.4s;
        -moz-transition: ease-out 0.4s;
        transition: ease-out 0.4s;
    }
    #skipIntro #btnSkipSegmentText {
        padding-right: 3px;
        padding-bottom: 2px;
    }
    @media (max-width: 1080px) {
        #skipIntro {
            right: 10%;
        }
    }
    #skipIntro:hover {
        box-shadow: inset 400px 0 0 0 #f9f9f9;
        -webkit-transition: ease-in 1s;
        -moz-transition: ease-in 1s;
        transition: ease-in 1s;
    }
    `;
    document.querySelector("head").appendChild(styleElement);
}
/**
 * Inject the skip intro button into the video player.
 * Calling this function is a no-op if the CSS has already been injected.
 */
introSkipper.injectButton = async function () {
    // Ensure the button we're about to inject into the page doesn't conflict with a pre-existing one
    const preExistingButton = introSkipper.testElement("div.skipIntro");
    if (preExistingButton) {
        preExistingButton.style.display = "none";
    }
    if (introSkipper.testElement(".btnSkipIntro.injected")) {
        introSkipper.d("Button already added");
        return;
    }
    introSkipper.d("Adding button");
    let config = await introSkipper.secureFetch("Intros/UserInterfaceConfiguration");
    if (!config.SkipButtonVisible) {
        introSkipper.d("Not adding button: not visible");
        return;
    }
    // Construct the skip button div
    const button = document.createElement("div");
    button.id = "skipIntro"
    button.classList.add("hide");
    button.addEventListener("click", introSkipper.doSkip);
    button.innerHTML = `
    <button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light injected">
        <span id="btnSkipSegmentText"></span>
        <span class="material-icons skip_next"></span>
    </button>
    `;
    button.dataset["intro_text"] = config.SkipButtonIntroText;
    button.dataset["credits_text"] = config.SkipButtonEndCreditsText;
    /*
    * Alternative workaround for #44. Jellyfin's video component registers a global click handler
    * (located at src/controllers/playback/video/index.js:1492) that pauses video playback unless
    * the clicked element has a parent with the class "videoOsdBottom" or "upNextContainer".
    */
    button.classList.add("upNextContainer");
    // Append the button to the video OSD
    let controls = document.querySelector("div#videoOsdPage");
    controls.appendChild(button);
}
/** Tests if the OSD controls are visible. */
introSkipper.osdVisible = function () {
    const osd = document.querySelector("div.videoOsdBottom");
    return osd ? !osd.classList.contains("hide") : false;
}
/** Get the currently playing skippable segment. */
introSkipper.getCurrentSegment = function (position) {
    for (let key in introSkipper.skipSegments) {
        const segment = introSkipper.skipSegments[key];
        if ((position >= segment.ShowSkipPromptAt && position < segment.HideSkipPromptAt) || (introSkipper.osdVisible() && position >= segment.IntroStart && position < segment.IntroEnd)) {
            segment["SegmentType"] = key;
            return segment;
        }
    }
    return { "SegmentType": "None" };
}
/** Playback position changed, check if the skip button needs to be displayed. */
introSkipper.videoPositionChanged = function () {
    const skipButton = document.querySelector("#skipIntro");
    if (!skipButton) {
        return;
    }
    const segment = introSkipper.getCurrentSegment(introSkipper.videoPlayer.currentTime);
    switch (segment["SegmentType"]) {
        case "None":
            if (skipButton.style.opacity === '0') return;

            skipButton.style.opacity = '0';
            skipButton.addEventListener("transitionend", () => {
                skipButton.classList.add("hide");
            }, { once: true });
            return;
        case "Introduction":
            skipButton.querySelector("#btnSkipSegmentText").textContent =
                skipButton.dataset["intro_text"];
            break;
        case "Credits":
            skipButton.querySelector("#btnSkipSegmentText").textContent =
                skipButton.dataset["credits_text"];
            break;
    }
    if (!skipButton.classList.contains("hide")) return;

    skipButton.classList.remove("hide");
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            skipButton.style.opacity = '1';
        });
    });
}
/** Seeks to the end of the intro. */
introSkipper.doSkip = function (e) {
    introSkipper.d("Skipping intro");
    introSkipper.d(introSkipper.skipSegments);
    const segment = introSkipper.getCurrentSegment(introSkipper.videoPlayer.currentTime);
    if (segment["SegmentType"] === "None") {
        console.warn("[intro skipper] doSkip() called without an active segment");
        return;
    }
    introSkipper.videoPlayer.currentTime = segment["IntroEnd"];
}
/** Tests if an element with the provided selector exists. */
introSkipper.testElement = function (selector) { return document.querySelector(selector); }
/** Make an authenticated fetch to the Jellyfin server and parse the response body as JSON. */
introSkipper.secureFetch = async function (url) {
    url = ApiClient.serverAddress() + "/" + url;
    const reqInit = { headers: { "Authorization": "MediaBrowser Token=" + ApiClient.accessToken() } };
    const res = await fetch(url, reqInit);
    if (res.status !== 200) { throw new Error(`Expected status 200 from ${url}, but got ${res.status}`); }
    return await res.json();
}
introSkipper.eventHandler = function (e) {
    console.log(e);
    if (!introSkipper.allowEnter) {
        event.preventDefault();
    }
    else if (e.key === "Enter" && document.querySelector("#skipIntro").style.opacity !== '0') {
        e.preventDefault();
        e.stopPropagation();
        introSkipper.doSkip();
        introSkipper.allowEnter = false
        setTimeout(() => {
          introSkipper.allowEnter = true;
        }, 5000);
    }
}
introSkipper.setup();

It works on LG!

from intro-skipper.

rlauuzo avatar rlauuzo commented on August 12, 2024 1

I tried it. Pressing the button with a remote/controller also pauses the video if the OSD isn't visible. I'm unsure how to prevent that from happening.

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

It's not easy to do. How about waiting for, say, 3 seconds and then clicking automatically?

from intro-skipper.

AbandonedCart avatar AbandonedCart commented on August 12, 2024

Wouldn't that be the same as auto skip, though? I think what you mean is an inverted version of the button where you click to keep it from skipping.

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

Wouldn't that be the same as auto skip, though?

true

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

Not possible without creating our own docker image.

from intro-skipper.

Bretterteig avatar Bretterteig commented on August 12, 2024

Please note I am not a developer.

Wouldn't something like skipButton.focus({ focusVisible: true }); next to the unhide line suffice?

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

extrem dirty hack

let introSkipper = {
    skipSegments: {},
    videoPlayer: {},
    // .bind() is used here to prevent illegal invocation errors
    originalFetch: window.fetch.bind(window),
};
introSkipper.d = function (msg) {
    console.debug("[intro skipper] ", msg);
  }
  /** Setup event listeners */
  introSkipper.setup = function () {
    document.addEventListener("viewshow", introSkipper.viewShow);
    window.fetch = introSkipper.fetchWrapper;
    introSkipper.d("Registered hooks");
  }
  /** Wrapper around fetch() that retrieves skip segments for the currently playing item. */
  introSkipper.fetchWrapper = async function (...args) {
    // Based on JellyScrub's trickplay.js
    let [resource, options] = args;
    let response = await introSkipper.originalFetch(resource, options);
    // Bail early if this isn't a playback info URL
    try {
      let path = new URL(resource).pathname;
      if (!path.includes("/PlaybackInfo")) { return response; }
      introSkipper.d("Retrieving skip segments from URL");
      introSkipper.d(path);
      
      // Check for context root and set id accordingly
      let path_arr = path.split("/");
      let id = "";
      if (path_arr[1] == "Items") {
        id = path_arr[2];
      } else {
        id = path_arr[3];
      }

      introSkipper.skipSegments = await introSkipper.secureFetch(`Episode/${id}/IntroSkipperSegments`);
      introSkipper.d("Successfully retrieved skip segments");
      introSkipper.d(introSkipper.skipSegments);
    }
    catch (e) {
      console.error("Unable to get skip segments from", resource, e);
    }
    return response;
  }
  /**
  * Event handler that runs whenever the current view changes.
  * Used to detect the start of video playback.
  */
  introSkipper.viewShow = function () {
    const location = window.location.hash;
    introSkipper.d("Location changed to " + location);
    if (location !== "#/video") {
      introSkipper.d("Ignoring location change");
      return;
    }
    introSkipper.injectCss();
    introSkipper.injectButton();
    introSkipper.videoPlayer = document.querySelector("video");
    if (introSkipper.videoPlayer != null) {
      introSkipper.d("Hooking video timeupdate");
      introSkipper.videoPlayer.addEventListener("timeupdate", introSkipper.videoPositionChanged);
    }
  }
  /**
  * Injects the CSS used by the skip intro button.
  * Calling this function is a no-op if the CSS has already been injected.
  */
  introSkipper.injectCss = function () {
    if (introSkipper.testElement("style#introSkipperCss")) {
      introSkipper.d("CSS already added");
      return;
    }
    introSkipper.d("Adding CSS");
    let styleElement = document.createElement("style");
    styleElement.id = "introSkipperCss";
    styleElement.innerText = `
    @media (hover:hover) and (pointer:fine) {
        #skipIntro .paper-icon-button-light:hover:not(:disabled) {
            color: black !important;
            background-color: rgba(47, 93, 98, 0) !important;
        }
    }
    #skipIntro .paper-icon-button-light.show-focus:focus {
        transform: scale(1.04) !important;
    }
    #skipIntro.upNextContainer {
        width: unset;
    }
    #skipIntro {
        padding: 0 1px;
        position: absolute;
        right: 10em;
        bottom: 9em;
        background-color: rgba(25, 25, 25, 0.66);
        border: 1px solid;
        border-radius: 0px;
        display: inline-block;
        cursor: pointer;
        opacity: 0;
        box-shadow: inset 0 0 0 0 #f9f9f9;
        -webkit-transition: ease-out 0.4s;
        -moz-transition: ease-out 0.4s;
        transition: ease-out 0.4s;
    }
    #skipIntro #btnSkipSegmentText {
        padding-right: 3px;
        padding-bottom: 2px;
    }
    @media (max-width: 1080px) {
        #skipIntro {
            right: 10%;
        }
    }
    #skipIntro:hover {
        box-shadow: inset 400px 0 0 0 #f9f9f9;
        -webkit-transition: ease-in 1s;
        -moz-transition: ease-in 1s;
        transition: ease-in 1s;
    }
    `;
    document.querySelector("head").appendChild(styleElement);
}
/**
 * Inject the skip intro button into the video player.
 * Calling this function is a no-op if the CSS has already been injected.
 */
introSkipper.injectButton = async function () {
    // Ensure the button we're about to inject into the page doesn't conflict with a pre-existing one
    const preExistingButton = introSkipper.testElement("div.skipIntro");
    if (preExistingButton) {
        preExistingButton.style.display = "none";
    }
    if (introSkipper.testElement(".btnSkipIntro.injected")) {
        introSkipper.d("Button already added");
        return;
    }
    introSkipper.d("Adding button");
    let config = await introSkipper.secureFetch("Intros/UserInterfaceConfiguration");
    if (!config.SkipButtonVisible) {
        introSkipper.d("Not adding button: not visible");
        return;
    }
    // Construct the skip button div
    const button = document.createElement("div");
    button.id = "skipIntro"
    button.classList.add("hide");
    button.addEventListener("click", introSkipper.doSkip);
    button.innerHTML = `
    <button is="paper-icon-button-light" class="btnSkipIntro paper-icon-button-light injected">
        <span id="btnSkipSegmentText"></span>
        <span class="material-icons skip_next"></span>
    </button>
    `;
    button.dataset["intro_text"] = config.SkipButtonIntroText;
    button.dataset["credits_text"] = config.SkipButtonEndCreditsText;
    /*
    * Alternative workaround for #44. Jellyfin's video component registers a global click handler
    * (located at src/controllers/playback/video/index.js:1492) that pauses video playback unless
    * the clicked element has a parent with the class "videoOsdBottom" or "upNextContainer".
    */
    button.classList.add("upNextContainer");
    // Append the button to the video OSD
    let controls = document.querySelector("div#videoOsdPage");
    controls.appendChild(button);
}
/** Tests if the OSD controls are visible. */
introSkipper.osdVisible = function () {
    const osd = document.querySelector("div.videoOsdBottom");
    return osd ? !osd.classList.contains("hide") : false;
}
/** Get the currently playing skippable segment. */
introSkipper.getCurrentSegment = function (position) {
    for (let key in introSkipper.skipSegments) {
        const segment = introSkipper.skipSegments[key];
        if ((position >= segment.ShowSkipPromptAt && position < segment.HideSkipPromptAt) || (introSkipper.osdVisible() && position >= segment.IntroStart && position < segment.IntroEnd)) {
            segment["SegmentType"] = key;
            return segment;
        }
    }
    return { "SegmentType": "None" };
}
/** Playback position changed, check if the skip button needs to be displayed. */
introSkipper.videoPositionChanged = function () {
    const skipButton = document.querySelector("#skipIntro");
    if (!skipButton) {
        return;
    }
    const segment = introSkipper.getCurrentSegment(introSkipper.videoPlayer.currentTime);
    switch (segment["SegmentType"]) {
        case "None":
            if (skipButton.style.opacity === '0') return;

            skipButton.style.opacity = '0';
            skipButton.addEventListener("transitionend", () => {
                skipButton.classList.add("hide");
            }, { once: true });
+           introSkipper.videoPlayer.play();
+           document.body.removeEventListener('keydown', introSkipper.doSkip, true);
+           introSkipper.videoPlayer.play();
            return;
        case "Introduction":
            skipButton.querySelector("#btnSkipSegmentText").textContent =
                skipButton.dataset["intro_text"];
+          document.body.addEventListener('keydown', introSkipper.doSkip, true);
            break;
        case "Credits":
            skipButton.querySelector("#btnSkipSegmentText").textContent =
                skipButton.dataset["credits_text"];
+          document.body.addEventListener('keydown', introSkipper.doSkip, true);
            break;
    }
    if (!skipButton.classList.contains("hide")) return;

    skipButton.classList.remove("hide");
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            skipButton.style.opacity = '1';
        });
    });
}
/** Seeks to the end of the intro. */
introSkipper.doSkip = function (e) {
    introSkipper.d("Skipping intro");
    introSkipper.d(introSkipper.skipSegments);
    const segment = introSkipper.getCurrentSegment(introSkipper.videoPlayer.currentTime);
    if (segment["SegmentType"] === "None") {
        console.warn("[intro skipper] doSkip() called without an active segment");
        return;
    }
    introSkipper.videoPlayer.currentTime = segment["IntroEnd"];
}
/** Tests if an element with the provided selector exists. */
introSkipper.testElement = function (selector) { return document.querySelector(selector); }
/** Make an authenticated fetch to the Jellyfin server and parse the response body as JSON. */
introSkipper.secureFetch = async function (url) {
    url = ApiClient.serverAddress() + "/" + url;
    const reqInit = { headers: { "Authorization": "MediaBrowser Token=" + ApiClient.accessToken() } };
    const res = await fetch(url, reqInit);
    if (res.status !== 200) { throw new Error(`Expected status 200 from ${url}, but got ${res.status}`); }
    return await res.json();
}
introSkipper.setup();

from intro-skipper.

AbandonedCart avatar AbandonedCart commented on August 12, 2024

Something like that should definitely be filtered for the platform, though. Possibly a separate instance of the case that includes a condition.

case "None" and platform:
 case "None":

from intro-skipper.

rlauuzo avatar rlauuzo commented on August 12, 2024

Would if (document.body.classList.contains('layout-tv')) be enough to restrict it to tvs and consoles?

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

or install the keydown event listener once with a debounce and redirect to the pause button if the skip button is not visible.

from intro-skipper.

AbandonedCart avatar AbandonedCart commented on August 12, 2024

Would if (document.body.classList.contains('layout-tv')) be enough to restrict it to tvs and consoles?

Probably. Every suggestion I've found eventually leads to @media tv

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

lol on amazon prime the skip button for the intro does not have a focus on the web version.

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

it's in v0.2.0.5

from intro-skipper.

Bretterteig avatar Bretterteig commented on August 12, 2024

Just installed .5. Skipping was working, however leaving the stream immediatly did prohibit me from selectibg menu items (without cursor on LG webOS)

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

Just installed .5. Skipping was working, however leaving the stream immediatly did prohibit me from selectibg menu items (without cursor on LG webOS)

Nach Drücken der Enter-Taste (LG Fernbedienung) ist die Eingabe für 5 Sekunden gesperrt.

from intro-skipper.

AbandonedCart avatar AbandonedCart commented on August 12, 2024

That’s why I was suggesting moving it to the completion of onSkip

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

Sorry but I dont understand the problem

from intro-skipper.

Bretterteig avatar Bretterteig commented on August 12, 2024

After skipping the intro:

  • OSD controls work at some point
  • Going back to the menu (leaving the player) the focused items are not reacting to the return key.

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

After skipping the intro:

* OSD controls work at some point

* Going back to the menu (leaving the player) the focused items are not reacting to the return key.

can you wait 5 seconds and try "Going back to the menu (leaving the player)" again?

from intro-skipper.

Bretterteig avatar Bretterteig commented on August 12, 2024

Does not change the behaviour.
When skipping I can see the button reappear for like .1 sec. Could this be related?

from intro-skipper.

jumoog avatar jumoog commented on August 12, 2024

I have clarified the problem in the discord call. I will roll back the commit for now and create a new version

from intro-skipper.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.