diff --git a/js/reddit-image-inline.user.js b/js/reddit-image-inline.user.js index 4de12bb..44e408c 100644 --- a/js/reddit-image-inline.user.js +++ b/js/reddit-image-inline.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Inline Reddit images and Comment Redirect // @namespace https://www.reddit.com -// @version 2026-08-28 +// @version 2026-08-28.1 // @description Embed images and videos posted as comments and redirect post links to comments // @author luxick // @updateURL https://git.luxick.de/luxick/scripts/js/raw/branch/master/reddit-image-inline.user.js @@ -30,20 +30,116 @@ return links; } - // Reddit's video comments link to an embedded player page rather than to a - // playable file, e.g. ".../video//player". - function isPlayerUrl(href) { - return /\/player\/?(\?|$)/i.test(href); + // Comment media is linked through Reddit's viewer: + // "https://reddit.com/media?url=" + function unwrapMediaUrl(href) { + let match = href.match(/[?&]url=([^&]+)/); + if (!match) { + return href; + } + try { + return decodeURIComponent(match[1]); + } catch (e) { + return match[1]; + } } - function buildPlayerFrame(href) { - let frame = document.createElement('iframe'); - frame.src = href; - frame.width = 300; - frame.height = 300; - frame.allowFullscreen = true; - frame.style = "max-width: 300px; border: none"; - return frame; + // Video comments point at an HLS playlist, which only Safari plays without + // a library. The same upload is also served as standalone MP4 renditions, + // so use those instead. + // + // Newer uploads name them CMAF_.mp4, older ones DASH_.mp4, + // and no upload has every height. Missing names return 403, so just walk + // the list until one loads. 720 first as a sane default for a 300px embed; + // 1080 last, only worth fetching when nothing smaller exists. + const HEIGHTS = [720, 480, 360, 270, 240, 220, 1080]; + const FAMILIES = ['CMAF', 'DASH']; + const AUDIO_NAMES = [ + 'CMAF_AUDIO_128', 'CMAF_AUDIO_64', + 'DASH_AUDIO_128', 'DASH_AUDIO_64', 'DASH_audio', + ]; + + function mediaUrls(id, names) { + return names.map(name => 'https://v.redd.it/' + id + '/' + name + '.mp4'); + } + + function videoNames() { + let names = []; + for (let family of FAMILIES) { + for (let height of HEIGHTS) { + names.push(family + '_' + height); + } + } + return names; + } + + function vredditId(url) { + let match = url.match(/^https?:\/\/v\.redd\.it\/([A-Za-z0-9]+)/i); + return match ? match[1] : null; + } + + // Move to the next candidate URL whenever the current one fails to load. + function chainSources(media, sources, onExhausted) { + let index = 0; + media.src = sources[0]; + media.addEventListener('error', function() { + index += 1; + if (index < sources.length) { + media.src = sources[index]; + media.load(); + } else if (onExhausted) { + onExhausted(); + } + }); + } + + // The MP4 renditions carry the video track only; the audio is a separate + // file that has to be kept in step with the video element by hand. + function buildAudio(video, id) { + let audio = document.createElement('audio'); + audio.preload = 'metadata'; + chainSources(audio, mediaUrls(id, AUDIO_NAMES)); + + video.addEventListener('play', function() { + audio.currentTime = video.currentTime; + audio.play().catch(() => {}); + }); + video.addEventListener('pause', () => audio.pause()); + video.addEventListener('seeking', () => { audio.currentTime = video.currentTime; }); + video.addEventListener('ratechange', () => { audio.playbackRate = video.playbackRate; }); + video.addEventListener('volumechange', function() { + audio.volume = video.volume; + audio.muted = video.muted; + }); + return audio; + } + + function buildVideo(href) { + let container = document.createElement('span'); + let mediaUrl = unwrapMediaUrl(href); + let id = vredditId(mediaUrl); + + let video = document.createElement('video'); + video.controls = true; + video.preload = 'metadata'; + video.style = "max-width: 300px"; + + let sources = id ? mediaUrls(id, videoNames()) : [mediaUrl]; + + chainSources(video, sources, function() { + console.log("no playable source for " + href); + let anchor = document.createElement('a'); + anchor.href = href; + anchor.target = '_blank'; + anchor.textContent = '