video support

This commit is contained in:
2025-06-16 09:02:18 +10:00
parent 8b0950b538
commit 4c675dcc71
2 changed files with 134 additions and 27 deletions

View File

@@ -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>

View File

@@ -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") {
// 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> </a>
` `;
).join("") } 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,7 +450,35 @@ 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 => {
// --- 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);
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, author: item.post.author.displayName,
handle: item.post.author.handle, handle: item.post.author.handle,
avatar: item.post.author.avatar, avatar: item.post.author.avatar,
@@ -383,14 +487,11 @@ async function fetchPosts() {
likes: item.post.likeCount || 0, likes: item.post.likeCount || 0,
reposts: item.post.repostCount || 0, reposts: item.post.repostCount || 0,
url: `https://bsky.app/profile/${item.post.author.handle}/post/${item.post.uri.split("/").pop()}`, url: `https://bsky.app/profile/${item.post.author.handle}/post/${item.post.uri.split("/").pop()}`,
embed: item.post.embed?.images?.map(img => ({ embed: embeds
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`); 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