Comments (22)
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.
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.
It's not easy to do. How about waiting for, say, 3 seconds and then clicking automatically?
from intro-skipper.
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.
Wouldn't that be the same as auto skip, though?
true
from intro-skipper.
Not possible without creating our own docker image.
from intro-skipper.
Please note I am not a developer.
Wouldn't something like skipButton.focus({ focusVisible: true });
next to the unhide line suffice?
from intro-skipper.
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.
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.
Would if (document.body.classList.contains('layout-tv'))
be enough to restrict it to tvs and consoles?
from intro-skipper.
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.
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.
lol on amazon prime the skip button for the intro does not have a focus on the web version.
from intro-skipper.
it's in v0.2.0.5
from intro-skipper.
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.
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.
That’s why I was suggesting moving it to the completion of onSkip
from intro-skipper.
Sorry but I dont understand the problem
from intro-skipper.
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.
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.
Does not change the behaviour.
When skipping I can see the button reappear for like .1 sec. Could this be related?
from intro-skipper.
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)
- [Bug]: all credits are invalid HOT 1
- [Bug]: Introskip Scheduled Task Fails Instantly HOT 11
- store data in /config/data HOT 3
- 10.9 autoskip funktion isn't working (skip button is issue #104) HOT 47
- Intro-Skipper Plugin Settings not Saving[Bug]: HOT 10
- [Bug]: Memory leak HOT 5
- [Bug]: Scheduled tasks only scan and detects intros (or outros) for a small part of "Shows" type library HOT 14
- [Feature request]: Add the option to exclude selected shows from automatic intro skip HOT 6
- [Bug]: Plugin failed to run HOT 8
- [Bug]: No skip for credits HOT 6
- Shows with Same Name Don't All Appear in Manage Fingerprints[Bug]: HOT 22
- [Bug]: Access to the path '/usr/share/jellyfin/web/index.html' is denied. HOT 2
- [Bug]: Plugin fails to load after 2.0.3 HOT 11
- [Bug]: Status shows 'NotSupported" after update .8 version HOT 8
- [Bug]: Jellyfin after update to 0.2.0.8 is not supported (jellyfin 10.9.2) HOT 6
- [Bug]: Status NotSupported HOT 9
- new generic api for Intro/Credit HOT 3
- research namespace change
- [Bug]: Play intro for first episode of a season is issued HOT 3
- [Bug]: No skip button or autoskip HOT 9
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from intro-skipper.