mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-25 19:26:03 +10:00
video support
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
<meta name="description" content="<!-- ::META_DESCRIPTION:: -->">
|
<meta name="description" content="<!-- ::META_DESCRIPTION:: -->">
|
||||||
<meta name="author" content="Litruv">
|
<meta name="author" content="Litruv">
|
||||||
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; <!-- ::PAGE_CSP_OTHER_DIRECTIVES:: -->">
|
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; worker-src 'self' blob:; <!-- ::PAGE_CSP_OTHER_DIRECTIVES:: -->">
|
||||||
|
|
||||||
<meta property="og:title" content="<!-- ::OG_TITLE:: -->">
|
<meta property="og:title" content="<!-- ::OG_TITLE:: -->">
|
||||||
<meta property="og:description" content="<!-- ::OG_DESCRIPTION:: -->">
|
<meta property="og:description" content="<!-- ::OG_DESCRIPTION:: -->">
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<!-- ::PAGE_SPECIFIC_STYLES:: -->
|
<!-- ::PAGE_SPECIFIC_STYLES:: -->
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.7/hls.min.js" defer></script>
|
||||||
<script src="main.js" defer></script> <!-- Common main script -->
|
<script src="main.js" defer></script> <!-- Common main script -->
|
||||||
<script src="dynamic-rotation.js" defer></script> <!-- Common main script -->
|
<script src="dynamic-rotation.js" defer></script> <!-- Common main script -->
|
||||||
<script src="tape.js" defer></script>
|
<script src="tape.js" defer></script>
|
||||||
|
|||||||
158
website/main.js
158
website/main.js
@@ -36,6 +36,9 @@ function getPostItColorByDate(dateString) {
|
|||||||
return postItColors[colorIndex];
|
return postItColors[colorIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global variable to track last user-set volume (default to 1)
|
||||||
|
let lastMediaVolume = 1;
|
||||||
|
|
||||||
function createPost(t) {
|
function createPost(t) {
|
||||||
const post = document.createElement("div");
|
const post = document.createElement("div");
|
||||||
post.className = "post";
|
post.className = "post";
|
||||||
@@ -60,15 +63,38 @@ function createPost(t) {
|
|||||||
link.target = "_blank";
|
link.target = "_blank";
|
||||||
link.style.textDecoration = "none";
|
link.style.textDecoration = "none";
|
||||||
|
|
||||||
const embedHtml = t.embed.length > 0
|
// --- EMBED HTML (images and videos) ---
|
||||||
? t.embed.map(img =>
|
let embedHtml = "";
|
||||||
`
|
if (t.embed && t.embed.length > 0) {
|
||||||
<a href="${img.url}" target="_blank">
|
embedHtml = t.embed.map((embed, idx) => {
|
||||||
<img class="image-placeholder" src="${img.url}" alt="${img.alt || "Image"}" style="width: 100%;" data-fullres="${img.fullres || img.url}" />
|
if (embed.type === "image") {
|
||||||
</a>
|
// Image embed
|
||||||
`
|
return `
|
||||||
).join("")
|
<a href="${embed.url}" target="_blank">
|
||||||
: "";
|
<img class="image-placeholder" src="${embed.url}" alt="${embed.alt || "Image"}" style="width: 100%;" data-fullres="${embed.fullres || embed.url}" />
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
} else if (embed.type === "video") {
|
||||||
|
// Video embed
|
||||||
|
// Use a data attribute for m3u8 playlist, set src only if not m3u8
|
||||||
|
const isM3u8 = embed.playlist && embed.playlist.endsWith('.m3u8');
|
||||||
|
return `
|
||||||
|
<video
|
||||||
|
class="video-embed"
|
||||||
|
${isM3u8 ? `data-hls-src="${embed.playlist}"` : `src="${embed.playlist}"`}
|
||||||
|
poster="${embed.thumbnail || ""}"
|
||||||
|
controls
|
||||||
|
style="width: 100%; max-height: 60vh; margin: 12px 0; background: #000; border-radius: 8px;"
|
||||||
|
preload="none"
|
||||||
|
>
|
||||||
|
Sorry, your browser doesn't support embedded videos.
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
// ...other embed types can be handled here...
|
||||||
|
return "";
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
const textHtml = t.text.replace(/\n/g, "<br>");
|
const textHtml = t.text.replace(/\n/g, "<br>");
|
||||||
|
|
||||||
@@ -102,12 +128,57 @@ function createPost(t) {
|
|||||||
createShadowbox(post, img);
|
createShadowbox(post, img);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- HLS.js video support for .m3u8 ---
|
||||||
|
const videos = post.querySelectorAll('video[data-hls-src],video.video-embed');
|
||||||
|
videos.forEach(video => {
|
||||||
|
// Set initial volume to lastMediaVolume
|
||||||
|
video.volume = lastMediaVolume;
|
||||||
|
|
||||||
|
// Prevent drag-selection when interacting with video controls (esp. volume)
|
||||||
|
video.addEventListener('mousedown', function (e) {
|
||||||
|
// Only prevent drag if the event is on the controls bar (not the video area)
|
||||||
|
// This is a best-effort: always prevent drag for any mousedown on video controls
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for volume changes and sync to all players
|
||||||
|
video.addEventListener('volumechange', function () {
|
||||||
|
if (!isNaN(video.volume)) {
|
||||||
|
lastMediaVolume = video.volume;
|
||||||
|
// Sync all other videos on the page
|
||||||
|
document.querySelectorAll('video.video-embed').forEach(v => {
|
||||||
|
if (v !== video && Math.abs(v.volume - lastMediaVolume) > 0.01) {
|
||||||
|
v.volume = lastMediaVolume;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// HLS.js setup for m3u8
|
||||||
|
const hlsVideos = post.querySelectorAll('video[data-hls-src]');
|
||||||
|
if (hlsVideos.length > 0 && window.Hls) {
|
||||||
|
hlsVideos.forEach(video => {
|
||||||
|
const src = video.getAttribute('data-hls-src');
|
||||||
|
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
|
video.src = src;
|
||||||
|
} else if (window.Hls.isSupported()) {
|
||||||
|
const hls = new window.Hls();
|
||||||
|
hls.loadSource(src);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
} else {
|
||||||
|
video.outerHTML = '<div style="color:red;padding:1em;text-align:center;">Video format not supported</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// --- end HLS.js support ---
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
link.href = t.url;
|
link.href = t.url;
|
||||||
link.appendChild(post);
|
link.appendChild(post);
|
||||||
|
|
||||||
console.log(`[Main] Created post with ${t.embed.length} images`);
|
console.log(`[Main] Created post with ${t.embed.length} embeds`);
|
||||||
|
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
@@ -346,6 +417,11 @@ function getShortestColumn(columns) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function layoutPosts(postElements) {
|
function layoutPosts(postElements) {
|
||||||
|
// Prevent layout if any video is in fullscreen
|
||||||
|
if (document.fullscreenElement && document.fullscreenElement.tagName === "VIDEO") {
|
||||||
|
console.log('[Main] Skipping layoutPosts: video is fullscreen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const numCols = getNumColumns();
|
const numCols = getNumColumns();
|
||||||
const columns = createColumns(numCols);
|
const columns = createColumns(numCols);
|
||||||
postElements.forEach((postEl, index) => {
|
postElements.forEach((postEl, index) => {
|
||||||
@@ -374,23 +450,48 @@ async function fetchPosts() {
|
|||||||
if (!res.ok) throw new Error("Failed to fetch posts");
|
if (!res.ok) throw new Error("Failed to fetch posts");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
const posts = filterOriginalPosts(data.feed).map(item => ({
|
const posts = filterOriginalPosts(data.feed).map(item => {
|
||||||
author: item.post.author.displayName,
|
// --- EMBED HANDLING ---
|
||||||
handle: item.post.author.handle,
|
let embeds = [];
|
||||||
avatar: item.post.author.avatar,
|
const embedObj = item.post.embed;
|
||||||
createdAt: item.post.record.createdAt,
|
if (embedObj) {
|
||||||
text: item.post.record.text,
|
// Debug output (optional)
|
||||||
likes: item.post.likeCount || 0,
|
// console.log("[Main] Post embed:", embedObj);
|
||||||
reposts: item.post.repostCount || 0,
|
// console.log("[Main] Embed $type:", embedObj.$type);
|
||||||
url: `https://bsky.app/profile/${item.post.author.handle}/post/${item.post.uri.split("/").pop()}`,
|
|
||||||
embed: item.post.embed?.images?.map(img => ({
|
|
||||||
url: img.thumb,
|
|
||||||
alt: img.alt || "",
|
|
||||||
fullres: img.fullsize || img.full || img.thumb // fallback to thumb if no fullres
|
|
||||||
})) || []
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log(`[Main] Fetched ${posts.length} posts, ${posts.filter(p => p.embed.length > 0).length} have images`);
|
if (embedObj.$type === "app.bsky.embed.images#view" && Array.isArray(embedObj.images)) {
|
||||||
|
embeds = embedObj.images.map(img => ({
|
||||||
|
type: "image",
|
||||||
|
url: img.thumb,
|
||||||
|
alt: img.alt || "",
|
||||||
|
fullres: img.fullsize || img.full || img.thumb // fallback to thumb if no fullres
|
||||||
|
}));
|
||||||
|
} else if (embedObj.$type === "app.bsky.embed.video#view") {
|
||||||
|
embeds = [{
|
||||||
|
type: "video",
|
||||||
|
playlist: embedObj.playlist,
|
||||||
|
thumbnail: embedObj.thumbnail,
|
||||||
|
alt: embedObj.alt || "",
|
||||||
|
aspectRatio: embedObj.aspectRatio || null
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
// Optionally handle other embed types (external, record, etc) here
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
author: item.post.author.displayName,
|
||||||
|
handle: item.post.author.handle,
|
||||||
|
avatar: item.post.author.avatar,
|
||||||
|
createdAt: item.post.record.createdAt,
|
||||||
|
text: item.post.record.text,
|
||||||
|
likes: item.post.likeCount || 0,
|
||||||
|
reposts: item.post.repostCount || 0,
|
||||||
|
url: `https://bsky.app/profile/${item.post.author.handle}/post/${item.post.uri.split("/").pop()}`,
|
||||||
|
embed: embeds
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Main] Fetched ${posts.length} posts, ${posts.filter(p => p.embed.some(e => e.type === "image")).length} have images, ${posts.filter(p => p.embed.some(e => e.type === "video")).length} have videos`);
|
||||||
|
|
||||||
// Only create post elements once
|
// Only create post elements once
|
||||||
postElementsCache = posts.map(post => createPost(post));
|
postElementsCache = posts.map(post => createPost(post));
|
||||||
@@ -415,6 +516,11 @@ async function fetchPosts() {
|
|||||||
|
|
||||||
// Only re-layout on resize, do not re-fetch or re-create posts
|
// Only re-layout on resize, do not re-fetch or re-create posts
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
|
// Prevent layout if any video is in fullscreen
|
||||||
|
if (document.fullscreenElement && document.fullscreenElement.tagName === "VIDEO") {
|
||||||
|
console.log('[Main] Skipping resize layout: video is fullscreen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (postElementsCache) {
|
if (postElementsCache) {
|
||||||
layoutPosts(postElementsCache);
|
layoutPosts(postElementsCache);
|
||||||
// Re-add tape after layout changes
|
// Re-add tape after layout changes
|
||||||
|
|||||||
Reference in New Issue
Block a user