mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-24 02:36:02 +10:00
video support
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<meta name="description" content="<!-- ::META_DESCRIPTION:: -->">
|
||||
<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:description" content="<!-- ::OG_DESCRIPTION:: -->">
|
||||
@@ -31,6 +31,7 @@
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<!-- ::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="dynamic-rotation.js" defer></script> <!-- Common main 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];
|
||||
}
|
||||
|
||||
// Global variable to track last user-set volume (default to 1)
|
||||
let lastMediaVolume = 1;
|
||||
|
||||
function createPost(t) {
|
||||
const post = document.createElement("div");
|
||||
post.className = "post";
|
||||
@@ -60,15 +63,38 @@ function createPost(t) {
|
||||
link.target = "_blank";
|
||||
link.style.textDecoration = "none";
|
||||
|
||||
const embedHtml = t.embed.length > 0
|
||||
? t.embed.map(img =>
|
||||
`
|
||||
<a href="${img.url}" target="_blank">
|
||||
<img class="image-placeholder" src="${img.url}" alt="${img.alt || "Image"}" style="width: 100%;" data-fullres="${img.fullres || img.url}" />
|
||||
</a>
|
||||
`
|
||||
).join("")
|
||||
: "";
|
||||
// --- EMBED HTML (images and videos) ---
|
||||
let embedHtml = "";
|
||||
if (t.embed && t.embed.length > 0) {
|
||||
embedHtml = t.embed.map((embed, idx) => {
|
||||
if (embed.type === "image") {
|
||||
// Image embed
|
||||
return `
|
||||
<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>");
|
||||
|
||||
@@ -102,12 +128,57 @@ function createPost(t) {
|
||||
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);
|
||||
|
||||
link.href = t.url;
|
||||
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;
|
||||
}
|
||||
@@ -346,6 +417,11 @@ function getShortestColumn(columns) {
|
||||
}
|
||||
|
||||
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 columns = createColumns(numCols);
|
||||
postElements.forEach((postEl, index) => {
|
||||
@@ -374,23 +450,48 @@ async function fetchPosts() {
|
||||
if (!res.ok) throw new Error("Failed to fetch posts");
|
||||
const data = await res.json();
|
||||
|
||||
const posts = filterOriginalPosts(data.feed).map(item => ({
|
||||
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: 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
|
||||
})) || []
|
||||
}));
|
||||
const posts = filterOriginalPosts(data.feed).map(item => {
|
||||
// --- EMBED HANDLING ---
|
||||
let embeds = [];
|
||||
const embedObj = item.post.embed;
|
||||
if (embedObj) {
|
||||
// Debug output (optional)
|
||||
// console.log("[Main] Post embed:", embedObj);
|
||||
// console.log("[Main] Embed $type:", embedObj.$type);
|
||||
|
||||
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
|
||||
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
|
||||
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) {
|
||||
layoutPosts(postElementsCache);
|
||||
// Re-add tape after layout changes
|
||||
|
||||
Reference in New Issue
Block a user