mediagoblin-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Fwd: Re: bug#47260: Package GNU MediaGoblin as a Guix service


From: Dr. Arne Babenhauserheide
Subject: Re: Fwd: Re: bug#47260: Package GNU MediaGoblin as a Guix service
Date: Thu, 06 May 2021 08:18:08 +0200
User-agent: mu4e 1.4.15; emacs 28.0.50

Hi,

I just added non-flickering video-change to the m3u-player. Attaching
the file. I thought that could be useful for MediaGoblin. The file is
attached.


Best wishes,
Arne


Attachment: m3u-player.js
Description: Binary data

Dr. Arne Babenhauserheide <arne_bab@web.de> writes:

> The followup-email with updated code:
>
> Ben Sturmfels <ben@sturm.com.au> writes:
>> On Mon, 22 Mar 2021, Dr. Arne Babenhauserheide wrote:
>>
>>> If you need support for m3u-playlists, you can use the player I wrote
>>> here: https://www.draketo.de/software/m3u-player
>>> → https://www.draketo.de/software/m3u-player.js (save as utf-8)
>>> (that m3u-playlists aren’t supported out of the box in most players is a
>>> strange oversight, the code adds it for video- and audio-tags, License:
>>> GPLv2 or later — just ask me if you need something else)
>>>
>>> There’s also an enhanced version for Freenet, but that has lots of
>>> performance-changes to work over high-latency networks and with paranoid
>>> CSP-settings:
>>> https://github.com/freenet/fred/pull/721/files#diff-33cbf95723ae7b33eb205cf9adc3411b2098e27ba757e553406f689a4fafb802
>>
>> Thanks Arne! I've forwarded this on to mediagoblin-devel@gnu.org so we
>> don't lose track of it.
>
> I added one change last week to support mobile browsers which answer
> "maybe" to the query `mediaTag.canPlayType('audio/x-mpegurl')` (yes,
> seriously, and it is in the spec :-) ).
>
> Also I backported the not freenet specific changes:
> - prefetch the next three tracks as blob and keep at most 10 tracks
>   cached to allow for fast track skipping (and now actually release the
>   memory)
> - adjustments to allow for inlining and survive the non-utf8-encoding.
> - continue automatically when fetch succeeded if playback was stopped
>   because it reached the end (but not if paused).
> - minimal mouseover for the back and forward arrows.
>
> When a https-m3u-list refers to a http-file, it falls back from fetching
> blobs to rewriting the src-part of the tag (because blobs cannot be
> fetched from a less secure resource).
>
> This is how it looks: https://www.draketo.de/software/m3u-player.html
>
> The changes are included in https://www.draketo.de/software/m3u-player.js
>
> You can use it like this:
>
> <script src="m3u-player.js" defer="defer"></script>
> <audio src="m3u-player-example-playlist.m3u" controls="controls">
> not supported?
> </audio>
>
>
> To make this bug-report independent of my site, here’s the full code:
>
>
>
> // [[file:m3u-player.org::*The script][The script:1]]
> // @license 
> magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt 
> GPL-v2-or-Later
> const nodes = document.querySelectorAll("audio,video");
> const playlists = {};
> const prefetchedTracks = new Map(); // use a map for insertion order, so we 
> can just blow away old entries.
> // maximum prefetched blobs that are kept.
> const MAX_PREFETCH_KEEP = 10;
> // maximum allowed number of entries in a playlist to prevent OOM attacks 
> against the browser with self-referencing playlists
> 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;
> }
> function isPlaylist(link) {
>   const linkHref = stripUrlParameters(link);
>   return linkHref.endsWith(".m3u") || linkHref.endsWith(".m3u8");
> }
> function isBlob(link) {
>   return new URL(link, window.location).protocol == 'blob';
> }
> function parsePlaylist(textContent) {
>   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.
>  */
> 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();
> }
> 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 = () => {
>     prefetchedTracks.set(url, xhr.response);
>     if (onload) {
>       onload();
>     }
>   };
>   xhr.send();
> }
> 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");
>     mediaTag.setAttribute("src", url);
>     // 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());
>     }
>   }
> }
>
> /**
>  * 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 = "&lt;"; // not textContent, because we MUST escape
>                            // the tag here and textContent shows the
>                            // escaped version
>   left.onclick = () => changeTrack(mediaTag, -1);
>   right.innerHTML = "&gt;";
>   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);
>     }
>   }
> }
> document.addEventListener('DOMContentLoaded', () => {
>   const nodes = document.querySelectorAll("audio,video");
>   nodes.forEach(processTag);
> });
> // @license-end
> // The script:1 ends here
>
>
>
>
> Best wishes,
> Arne


-- 
Unpolitisch sein
heißt politisch sein
ohne es zu merken

Attachment: signature.asc
Description: PGP signature


reply via email to

[Prev in Thread] Current Thread [Next in Thread]