[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
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 = "<"; // 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);
> }
> }
> }
> 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
signature.asc
Description: PGP signature
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- Re: Fwd: Re: bug#47260: Package GNU MediaGoblin as a Guix service,
Dr. Arne Babenhauserheide <=