From f03224fe024cf78d8e9256ac6e7163c44ce32c84 Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-3VIS1T9\\junguler" Date: Fri, 5 Aug 2022 03:38:34 +0430 Subject: [PATCH] adjust playlist buttons position my work is still not done here but the player looks way better now --- stuff/m3u-player.js | 544 ++++++++++++++++++++++---------------------- stuff/style.css | 30 ++- 2 files changed, 299 insertions(+), 275 deletions(-) diff --git a/stuff/m3u-player.js b/stuff/m3u-player.js index eee04eb72..a88d15161 100755 --- a/stuff/m3u-player.js +++ b/stuff/m3u-player.js @@ -9,307 +9,307 @@ const MAX_PREFETCH_KEEP = 10; const MAX_PLAYLIST_LENGTH = 1000; const PLAYLIST_MIME_TYPES = ["audio/x-mpegurl", "audio/mpegurl", "application/vnd.apple.mpegurl","application/mpegurl","application/x-mpegurl"]; function stripUrlParameters(link) { - const url = new URL(link, window.location); - url.search = ""; - url.hash = ""; - return url.href; +const url = new URL(link, window.location); +url.search = ""; +url.hash = ""; +return url.href; } function isPlaylist(link) { - const linkHref = stripUrlParameters(link); - return linkHref.endsWith(".m3u") || linkHref.endsWith(".m3u8"); +const linkHref = stripUrlParameters(link); +return linkHref.endsWith(".m3u") || linkHref.endsWith(".m3u8"); } function isBlob(link) { - return new URL(link, window.location).protocol == 'blob'; +return new URL(link, window.location).protocol == 'blob'; } function parsePlaylist(textContent) { - return textContent.match(/^(?!#)(?!\s).*$/mg) - .filter(s => s); // filter removes empty strings +return textContent.match(/^(?!#)(?!\s).*$/mg) +.filter(s => s); // filter removes empty strings } /** - * Download the given playlist, parse it, and store the tracks in the - * global playlists object using the url as key. - * - * Runs callback once the playlist downloaded successfully. - */ +* Download the given playlist, parse it, and store the tracks in the +* global playlists object using the url as key. +* +* Runs callback once the playlist downloaded successfully. +*/ function fetchPlaylist(url, onload, onerror) { - const playlistFetcher = new XMLHttpRequest(); - playlistFetcher.open("GET", url, true); - playlistFetcher.responseType = "blob"; // to get a mime type - playlistFetcher.onload = () => { - if (PLAYLIST_MIME_TYPES.includes(playlistFetcher.response.type)) { // security check to ensure that filters have run - const reader = new FileReader(); - const load = onload; // propagate to inner scope - reader.addEventListener("loadend", e => { - playlists[url] = parsePlaylist(reader.result); - onload(); - }); - reader.readAsText(playlistFetcher.response); - } else { - console.error("playlist must have one of the playlist MIME type '" + PLAYLIST_MIME_TYPES + "' but it had MIME type '" + playlistFetcher.response.type + "'."); - onerror(); - } - }; - playlistFetcher.onerror = onerror; - playlistFetcher.abort = onerror; - playlistFetcher.send(); +const playlistFetcher = new XMLHttpRequest(); +playlistFetcher.open("GET", url, true); +playlistFetcher.responseType = "blob"; // to get a mime type +playlistFetcher.onload = () => { +if (PLAYLIST_MIME_TYPES.includes(playlistFetcher.response.type)) { // security check to ensure that filters have run +const reader = new FileReader(); +const load = onload; // propagate to inner scope +reader.addEventListener("loadend", e => { +playlists[url] = parsePlaylist(reader.result); +onload(); +}); +reader.readAsText(playlistFetcher.response); +} else { +console.error("playlist must have one of the playlist MIME type '" + PLAYLIST_MIME_TYPES + "' but it had MIME type '" + playlistFetcher.response.type + "'."); +onerror(); +} +}; +playlistFetcher.onerror = onerror; +playlistFetcher.abort = onerror; +playlistFetcher.send(); } function servedPartialDataAndCanRequestAll (xhr) { - if (xhr.status === 206) { - if (xhr.getResponseHeader("content-range").includes("/")) { - if (!xhr.getResponseHeader("content-range").includes("/*")) { - return true; - } - } - } - return false; +if (xhr.status === 206) { +if (xhr.getResponseHeader("content-range").includes("/")) { +if (!xhr.getResponseHeader("content-range").includes("/*")) { +return true; +} +} +} +return false; } function prefetchTrack(url, onload) { - if (prefetchedTracks.has(url)) { - return; - } - // first cleanup: kill the oldest entries until we're back at the allowed size - while (prefetchedTracks.size > MAX_PREFETCH_KEEP) { - const key = prefetchedTracks.keys().next().value; - const track = prefetchedTracks.get(key); - prefetchedTracks.delete(key); - } - // first set the prefetched to the url so we will never request twice - prefetchedTracks.set(url, url); - // now start replacing it with a blob - const xhr = new XMLHttpRequest(); - xhr.open("GET", url, true); - xhr.responseType = "blob"; - xhr.onload = () => { - if (servedPartialDataAndCanRequestAll(xhr)) { - const endRange = Number(xhr.getResponseHeader("content-range").split("/")[1]) - 1; - const rangeXhr = new XMLHttpRequest(); - rangeXhr.open("GET", url, true); - rangeXhr.responseType = "blob"; - rangeXhr.setRequestHeader("range", "bytes=0-" + endRange); - rangeXhr.onload = () => { - prefetchedTracks.set(url, rangeXhr.response); - if (onload) { - onload(); - } - }; - rangeXhr.send(); - } else { - prefetchedTracks.set(url, xhr.response); - if (onload) { - onload(); - } - } - }; - xhr.send(); +if (prefetchedTracks.has(url)) { +return; +} +// first cleanup: kill the oldest entries until we're back at the allowed size +while (prefetchedTracks.size > MAX_PREFETCH_KEEP) { +const key = prefetchedTracks.keys().next().value; +const track = prefetchedTracks.get(key); +prefetchedTracks.delete(key); +} +// first set the prefetched to the url so we will never request twice +prefetchedTracks.set(url, url); +// now start replacing it with a blob +const xhr = new XMLHttpRequest(); +xhr.open("GET", url, true); +xhr.responseType = "blob"; +xhr.onload = () => { +if (servedPartialDataAndCanRequestAll(xhr)) { +const endRange = Number(xhr.getResponseHeader("content-range").split("/")[1]) - 1; +const rangeXhr = new XMLHttpRequest(); +rangeXhr.open("GET", url, true); +rangeXhr.responseType = "blob"; +rangeXhr.setRequestHeader("range", "bytes=0-" + endRange); +rangeXhr.onload = () => { +prefetchedTracks.set(url, rangeXhr.response); +if (onload) { +onload(); +} +}; +rangeXhr.send(); +} else { +prefetchedTracks.set(url, xhr.response); +if (onload) { +onload(); +} +} +}; +xhr.send(); } function showStaticOverlay(mediaTag, canvas) { - if (mediaTag instanceof Audio) { - return; - } - // take screenshot of video and overlay it to mask short-term flicker. - const realWidth = mediaTag.getBoundingClientRect().width; - const realHeight = mediaTag.getBoundingClientRect().height; - canvas.width = realWidth; - canvas.height = realHeight; - // need the actual video size - const videoAspectRatio = mediaTag.videoHeight / mediaTag.videoWidth; - const tagAspectRatio = realHeight / realWidth; - const videoIsPartialHeight = tagAspectRatio > (videoAspectRatio * 1.01); // avoid rounding errors - const videoIsPartialWidth = videoAspectRatio > (tagAspectRatio * 1.01); // avoid rounding errors - if (videoIsPartialHeight) { - canvas.height = realWidth * videoAspectRatio; - } else if (videoIsPartialWidth) { - canvas.width = realHeight / videoAspectRatio; - } - const context = canvas.getContext("2d"); - context.scale(canvas.width / mediaTag.videoWidth, canvas.height / mediaTag.videoHeight); - context.drawImage(mediaTag, 0, 0); - canvas.hidden = true; - mediaTag.parentNode.insertBefore(canvas, mediaTag.nextSibling); - canvas.style.position = "absolute"; - // shift canvas to cover only the space where the video actually is - if (videoIsPartialWidth) { - canvas.style.marginLeft = "-" + ((realWidth + canvas.width) / 2.) + "px"; - } else { - canvas.style.marginLeft = "-" + realWidth + "px"; - } - if (videoIsPartialHeight) { - canvas.style.marginTop = ((realHeight - canvas.height) / 2.) + "px"; - } - canvas.hidden = false; +if (mediaTag instanceof Audio) { +return; +} +// take screenshot of video and overlay it to mask short-term flicker. +const realWidth = mediaTag.getBoundingClientRect().width; +const realHeight = mediaTag.getBoundingClientRect().height; +canvas.width = realWidth; +canvas.height = realHeight; +// need the actual video size +const videoAspectRatio = mediaTag.videoHeight / mediaTag.videoWidth; +const tagAspectRatio = realHeight / realWidth; +const videoIsPartialHeight = tagAspectRatio > (videoAspectRatio * 1.01); // avoid rounding errors +const videoIsPartialWidth = videoAspectRatio > (tagAspectRatio * 1.01); // avoid rounding errors +if (videoIsPartialHeight) { +canvas.height = realWidth * videoAspectRatio; +} else if (videoIsPartialWidth) { +canvas.width = realHeight / videoAspectRatio; +} +const context = canvas.getContext("2d"); +context.scale(canvas.width / mediaTag.videoWidth, canvas.height / mediaTag.videoHeight); +context.drawImage(mediaTag, 0, 0); +canvas.hidden = true; +mediaTag.parentNode.insertBefore(canvas, mediaTag.nextSibling); +canvas.style.position = "absolute"; +// shift canvas to cover only the space where the video actually is +if (videoIsPartialWidth) { +canvas.style.marginLeft = "-" + ((realWidth + canvas.width) / 2.) + "px"; +} else { +canvas.style.marginLeft = "-" + realWidth + "px"; +} +if (videoIsPartialHeight) { +canvas.style.marginTop = ((realHeight - canvas.height) / 2.) + "px"; +} +canvas.hidden = false; } function updateSrc(mediaTag, callback) { - const playlistUrl = mediaTag.getAttribute("playlist"); - const trackIndex = mediaTag.getAttribute("track-index"); - // deepcopy playlists to avoid shared mutation - let playlist = [...playlists[playlistUrl]]; - let trackUrl = playlist[trackIndex]; - // download and splice in playlists as needed - if (isPlaylist(trackUrl)) { - if (playlist.length >= MAX_PLAYLIST_LENGTH) { - // skip playlist if we already have too many tracks - changeTrack(mediaTag, +1); - } else { - // do not use the cached playlist here, though it is tempting: it might genuinely change to allow for updates - fetchPlaylist( - trackUrl, - () => { - playlist.splice(trackIndex, 1, ...playlists[trackUrl]); - playlists[playlistUrl] = playlist; - updateSrc(mediaTag, callback); - }, - () => callback()); - } - } else { - let url = prefetchedTracks.has(trackUrl) - ? prefetchedTracks.get(trackUrl) instanceof Blob - ? URL.createObjectURL(prefetchedTracks.get(trackUrl)) - : trackUrl : trackUrl; - const oldUrl = mediaTag.getAttribute("src"); - // prevent size flickering by setting height before src change - const canvas = document.createElement("canvas"); - if (!isNaN(mediaTag.duration) // already loaded a valid file - && document.fullscreen !== true) { // overlay does not work for fullscreen - // mask flickering with a static overlay - try { - showStaticOverlay(mediaTag, canvas); - } catch (error) { - console.log(error); - } - } - // force sizes to stay constant during loading of the next segment - mediaTag.style.height = mediaTag.getBoundingClientRect().height.toString() + 'px'; - mediaTag.style.width = mediaTag.getBoundingClientRect().width.toString() + 'px'; - // swich to the next segment - mediaTag.setAttribute("src", url); - mediaTag.oncanplaythrough = () => { - if (!isNaN(mediaTag.duration)) { // already loaded a valid file - // unset element styles to allow recomputation if sizes changed - mediaTag.style.height = null; - mediaTag.style.width = null; - } - // remove overlay - canvas.hidden = true; - canvas.remove(); // to allow garbage collection - }; - setTimeout(() => canvas.remove(), 300); // fallback - // replace the url when done, because a blob from an xhr request - // is more reliable in the media tag; - // the normal URL caused jumping prematurely to the next track. - if (url == trackUrl) { - prefetchTrack(trackUrl, () => { - if (mediaTag.paused) { - if (url == mediaTag.getAttribute("src")) { - if (mediaTag.currentTime === 0) { - mediaTag.setAttribute("src", URL.createObjectURL( - prefetchedTracks.get(url))); - } - } - } - }); - } - // allow releasing memory - if (isBlob(oldUrl)) { - URL.revokeObjectURL(oldUrl); - } - // update title - mediaTag.parentElement.querySelector(".m3u-player--title").title = trackUrl; - mediaTag.parentElement.querySelector(".m3u-player--title").textContent = trackUrl; - // start prefetching the next three tracks. - for (const i of [1, 2, 3]) { - if (playlist.length > Number(trackIndex) + i) { - prefetchTrack(playlist[Number(trackIndex) + i]); - } - } - callback(); - } +const playlistUrl = mediaTag.getAttribute("playlist"); +const trackIndex = mediaTag.getAttribute("track-index"); +// deepcopy playlists to avoid shared mutation +let playlist = [...playlists[playlistUrl]]; +let trackUrl = playlist[trackIndex]; +// download and splice in playlists as needed +if (isPlaylist(trackUrl)) { +if (playlist.length >= MAX_PLAYLIST_LENGTH) { +// skip playlist if we already have too many tracks +changeTrack(mediaTag, +1); +} else { +// do not use the cached playlist here, though it is tempting: it might genuinely change to allow for updates +fetchPlaylist( +trackUrl, +() => { +playlist.splice(trackIndex, 1, ...playlists[trackUrl]); +playlists[playlistUrl] = playlist; +updateSrc(mediaTag, callback); +}, +() => callback()); +} +} else { +let url = prefetchedTracks.has(trackUrl) +? prefetchedTracks.get(trackUrl) instanceof Blob +? URL.createObjectURL(prefetchedTracks.get(trackUrl)) +: trackUrl : trackUrl; +const oldUrl = mediaTag.getAttribute("src"); +// prevent size flickering by setting height before src change +const canvas = document.createElement("canvas"); +if (!isNaN(mediaTag.duration) // already loaded a valid file +&& document.fullscreen !== true) { // overlay does not work for fullscreen +// mask flickering with a static overlay +try { +showStaticOverlay(mediaTag, canvas); +} catch (error) { +console.log(error); +} +} +// force sizes to stay constant during loading of the next segment +mediaTag.style.height = mediaTag.getBoundingClientRect().height.toString() + 'px'; +mediaTag.style.width = mediaTag.getBoundingClientRect().width.toString() + 'px'; +// swich to the next segment +mediaTag.setAttribute("src", url); +mediaTag.oncanplaythrough = () => { +if (!isNaN(mediaTag.duration)) { // already loaded a valid file +// unset element styles to allow recomputation if sizes changed +mediaTag.style.height = null; +mediaTag.style.width = null; +} +// remove overlay +canvas.hidden = true; +canvas.remove(); // to allow garbage collection +}; +setTimeout(() => canvas.remove(), 300); // fallback +// replace the url when done, because a blob from an xhr request +// is more reliable in the media tag; +// the normal URL caused jumping prematurely to the next track. +if (url == trackUrl) { +prefetchTrack(trackUrl, () => { +if (mediaTag.paused) { +if (url == mediaTag.getAttribute("src")) { +if (mediaTag.currentTime === 0) { +mediaTag.setAttribute("src", URL.createObjectURL( +prefetchedTracks.get(url))); +} +} +} +}); +} +// allow releasing memory +if (isBlob(oldUrl)) { +URL.revokeObjectURL(oldUrl); +} +// update title +mediaTag.parentElement.querySelector(".m3u-player--title").title = trackUrl; +mediaTag.parentElement.querySelector(".m3u-player--title").textContent = trackUrl; +// start prefetching the next three tracks. +for (const i of [1, 2, 3]) { +if (playlist.length > Number(trackIndex) + i) { +prefetchTrack(playlist[Number(trackIndex) + i]); +} +} +callback(); +} } function changeTrack(mediaTag, diff) { - const currentTrackIndex = Number(mediaTag.getAttribute("track-index")); - const nextTrackIndex = currentTrackIndex + diff; - const tracks = playlists[mediaTag.getAttribute("playlist")]; - if (nextTrackIndex >= 0) { // do not collapse the if clauses with double-and, that does not survive inlining - if (tracks.length > nextTrackIndex) { - mediaTag.setAttribute("track-index", nextTrackIndex); - updateSrc(mediaTag, () => mediaTag.play()); - } - } +const currentTrackIndex = Number(mediaTag.getAttribute("track-index")); +const nextTrackIndex = currentTrackIndex + diff; +const tracks = playlists[mediaTag.getAttribute("playlist")]; +if (nextTrackIndex >= 0) { // do not collapse the if clauses with double-and, that does not survive inlining +if (tracks.length > nextTrackIndex) { +mediaTag.setAttribute("track-index", nextTrackIndex); +updateSrc(mediaTag, () => mediaTag.play()); +} +} } /** - * Turn a media tag into playlist player. - */ +* Turn a media tag into playlist player. +*/ function initPlayer(mediaTag) { - mediaTag.setAttribute("playlist", mediaTag.getAttribute("src")); - mediaTag.setAttribute("track-index", 0); - const url = mediaTag.getAttribute("playlist"); - const wrapper = mediaTag.parentElement.insertBefore(document.createElement("div"), mediaTag); - const controls = document.createElement("div"); - const left = document.createElement("span"); - const title = document.createElement("span"); - const right = document.createElement("span"); - controls.appendChild(left); - controls.appendChild(title); - controls.appendChild(right); - left.classList.add("m3u-player--left"); - right.classList.add("m3u-player--right"); - title.classList.add("m3u-player--title"); - title.style.overflow = "hidden"; - title.style.textOverflow = "ellipsis"; - title.style.whiteSpace = "nowrap"; - title.style.opacity = "0.3"; - title.style.direction = "rtl"; // for truncation on the left - title.style.paddingLeft = "0.5em"; - title.style.paddingRight = "0.5em"; - controls.style.display = "flex"; - controls.style.justifyContent = "space-between"; - const styleTag = document.createElement("style"); - styleTag.innerHTML = ".m3u-player--left:hover, .m3u-player--right:hover {color: wheat; background-color: DarkSlateGray}"; - wrapper.appendChild(styleTag); - wrapper.appendChild(controls); - controls.style.width = mediaTag.getBoundingClientRect().width.toString() + "px"; - // appending the media tag to the wrapper removes it from the outer scope but keeps the event listeners - wrapper.appendChild(mediaTag); - left.innerHTML = "<"; // not textContent, because we MUST escape - // the tag here and textContent shows the - // escaped version - left.onclick = () => changeTrack(mediaTag, -1); - right.innerHTML = ">"; - right.onclick = () => changeTrack(mediaTag, +1); - fetchPlaylist( - url, - () => { - updateSrc(mediaTag, () => null); - mediaTag.addEventListener("ended", event => { - if (mediaTag.currentTime >= mediaTag.duration) { - changeTrack(mediaTag, +1); - } - }); - }, - () => null); - // keep the controls aligned to the media tag - mediaTag.resizeObserver = new ResizeObserver(entries => { - controls.style.width = entries[0].contentRect.width.toString() + "px"; - }); - mediaTag.resizeObserver.observe(mediaTag); +mediaTag.setAttribute("playlist", mediaTag.getAttribute("src")); +mediaTag.setAttribute("track-index", 0); +const url = mediaTag.getAttribute("playlist"); +const wrapper = mediaTag.parentElement.insertBefore(document.createElement("div"), mediaTag); +const controls = document.createElement("div"); +const left = document.createElement("button"); +const title = document.createElement("span"); +const right = document.createElement("button"); +controls.appendChild(left); +controls.appendChild(title); +controls.appendChild(right); +left.classList.add("m3u-player--left"); +right.classList.add("m3u-player--right"); +title.classList.add("m3u-player--title"); +title.style.overflow = "hidden"; +title.style.textOverflow = "ellipsis"; +title.style.whiteSpace = "nowrap"; +// title.style.opacity = "1"; +// title.style.direction = "rtl"; // for truncation on the left +// title.style.paddingLeft = "0.5em"; +// title.style.paddingRight = "0.5em"; +controls.style.display = "flex"; +controls.style.justifyContent = "space-between"; +const styleTag = document.createElement("style"); +styleTag.innerHTML = ".m3u-player--left:hover, .m3u-player--right:hover {color: #ccc; background-color: #333}"; +wrapper.appendChild(styleTag); +wrapper.appendChild(controls); +controls.style.width = mediaTag.getBoundingClientRect().width.toString() + "px"; +// appending the media tag to the wrapper removes it from the outer scope but keeps the event listeners +wrapper.appendChild(mediaTag); +left.innerHTML = "<"; // not textContent, because we MUST escape +// the tag here and textContent shows the +// escaped version +left.onclick = () => changeTrack(mediaTag, -1); +right.innerHTML = ">"; +right.onclick = () => changeTrack(mediaTag, +1); +fetchPlaylist( +url, +() => { +updateSrc(mediaTag, () => null); +mediaTag.addEventListener("ended", event => { +if (mediaTag.currentTime >= mediaTag.duration) { +changeTrack(mediaTag, +1); +} +}); +}, +() => null); +// keep the controls aligned to the media tag +mediaTag.resizeObserver = new ResizeObserver(entries => { +controls.style.width = entries[0].contentRect.width.toString() + "px"; +}); +mediaTag.resizeObserver.observe(mediaTag); } function processTag(mediaTag) { - const canPlayClaim = mediaTag.canPlayType('audio/x-mpegurl'); - let supportsPlaylists = !!canPlayClaim; - if (canPlayClaim == 'maybe') { // yes, seriously: specced as you only know when you try - supportsPlaylists = false; - } - if (!supportsPlaylists) { - if (isPlaylist(mediaTag.getAttribute("src"))) { - initPlayer(mediaTag); - } - } +const canPlayClaim = mediaTag.canPlayType('audio/x-mpegurl'); +let supportsPlaylists = !!canPlayClaim; +if (canPlayClaim == 'maybe') { // yes, seriously: specced as you only know when you try +supportsPlaylists = false; +} +if (!supportsPlaylists) { +if (isPlaylist(mediaTag.getAttribute("src"))) { +initPlayer(mediaTag); +} +} } document.addEventListener('DOMContentLoaded', () => { - const nodes = document.querySelectorAll("audio,video"); - nodes.forEach(processTag); +const nodes = document.querySelectorAll("audio,video"); +nodes.forEach(processTag); }); // @license-end // The script:1 ends here diff --git a/stuff/style.css b/stuff/style.css index 65317e0c3..3fb03f2da 100755 --- a/stuff/style.css +++ b/stuff/style.css @@ -2,7 +2,7 @@ body { background:#222; color:#ccc; } - + .container{ display: flex; justify-content: center; @@ -14,10 +14,34 @@ transform: translate(-50%, -50%); } audio { -width:1000px; -height: 52px; +width: 1000px; } .m3u-player--left, .m3u-player--right, .m3u-player--title { font-size: 200%; +appearance: none; +} + +.m3u-player--left, .m3u-player--right { +cursor: pointer; +position: absolute; +top: 35px; +} + +.m3u-player--left { +margin-left: auto; +left: -44px; +} + +.m3u-player--right { +margin-right: auto; +right: -44px; +} + +.m3u-player--title { +margin: auto; +} + +button { +width: 40px; }