Added FLV Player

This commit is contained in:
2025-10-08 01:10:13 +05:30
parent f6be5084ad
commit 40ae8d5f3d
7 changed files with 282 additions and 38 deletions
+57 -34
View File
@@ -17,6 +17,7 @@
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.13",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"esbuild": "^0.25.10", "esbuild": "^0.25.10",
"flv.js": "^1.6.0",
"hls.js": "^1.6.13", "hls.js": "^1.6.13",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"typescript": "^5.9.2", "typescript": "^5.9.2",
@@ -59,9 +60,9 @@
} }
}, },
"node_modules/@antfu/utils": { "node_modules/@antfu/utils": {
"version": "9.2.1", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.2.1.tgz", "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz",
"integrity": "sha512-TMilPqXyii1AsiEii6l6ubRzbo76p6oshUSYPaKsmXDavyMLqjzVDkcp3pHp5ELMUNJHATcEOGxKTTsX9yYhGg==", "integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -2646,9 +2647,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide/node_modules/detect-libc": { "node_modules/@tailwindcss/oxide/node_modules/detect-libc": {
"version": "2.1.1", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -2917,9 +2918,9 @@
} }
}, },
"node_modules/@tailwindcss/vite/node_modules/detect-libc": { "node_modules/@tailwindcss/vite/node_modules/detect-libc": {
"version": "2.1.1", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -2999,12 +3000,12 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.6.2", "version": "24.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz",
"integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.13.0" "undici-types": "~7.14.0"
} }
}, },
"node_modules/@types/tar": { "node_modules/@types/tar": {
@@ -3805,9 +3806,9 @@
} }
}, },
"node_modules/ci-info": { "node_modules/ci-info": {
"version": "4.3.0", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -4474,6 +4475,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.10", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
@@ -4692,6 +4699,16 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/flv.js": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/flv.js/-/flv.js-1.6.2.tgz",
"integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==",
"license": "Apache-2.0",
"dependencies": {
"es6-promise": "^4.2.8",
"webworkify-webpack": "^2.1.5"
}
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -4713,9 +4730,9 @@
} }
}, },
"node_modules/fontace": { "node_modules/fontace": {
"version": "0.3.0", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/fontace/-/fontace-0.3.0.tgz", "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.3.1.tgz",
"integrity": "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg==", "integrity": "sha512-9f5g4feWT1jWT8+SbL85aLIRLIXUaDygaM2xPXRmzPYxrOMNok79Lr3FGJoKVNKibE0WCunNiEVG2mwuE+2qEg==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -5653,9 +5670,9 @@
} }
}, },
"node_modules/lightningcss/node_modules/detect-libc": { "node_modules/lightningcss/node_modules/detect-libc": {
"version": "2.1.1", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -6957,9 +6974,9 @@
} }
}, },
"node_modules/package-manager-detector": { "node_modules/package-manager-detector": {
"version": "1.3.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.0.tgz",
"integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", "integrity": "sha512-rRZ+pR1Usc+ND9M2NkmCvE/LYJS+8ORVV9X0KuNSY/gFsp7RBHJM/ADh9LYq4Vvfq6QkKrW6/weuh8SMEtN5gw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pako": { "node_modules/pako": {
@@ -7609,9 +7626,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -7665,9 +7682,9 @@
} }
}, },
"node_modules/sharp/node_modules/detect-libc": { "node_modules/sharp/node_modules/detect-libc": {
"version": "2.1.1", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"peer": true, "peer": true,
@@ -8030,9 +8047,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.13.0", "version": "7.14.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
"integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-properties": { "node_modules/unicode-properties": {
@@ -8734,6 +8751,12 @@
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true "peer": true
}, },
"node_modules/webworkify-webpack": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz",
"integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==",
"license": "MIT"
},
"node_modules/whatwg-encoding": { "node_modules/whatwg-encoding": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+1
View File
@@ -75,6 +75,7 @@
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.13",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"esbuild": "^0.25.10", "esbuild": "^0.25.10",
"flv.js": "^1.6.0",
"hls.js": "^1.6.13", "hls.js": "^1.6.13",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"typescript": "^5.9.2", "typescript": "^5.9.2",
+2
View File
@@ -16,6 +16,7 @@ export { default as Dropdown } from "../src/components/Dropdown.astro";
export { default as Field } from "../src/components/Field.astro"; export { default as Field } from "../src/components/Field.astro";
export { default as FieldRow } from "../src/components/FieldRow.astro"; export { default as FieldRow } from "../src/components/FieldRow.astro";
export { default as FileUploader } from "../src/components/FileUploader.astro"; export { default as FileUploader } from "../src/components/FileUploader.astro";
export { default as FLVPLayer } from "../src/components/FLVPLayer.astro";
export { default as Form } from "../src/components/Form.astro"; export { default as Form } from "../src/components/Form.astro";
export { default as HLSPLayer } from "../src/components/HLSPLayer.astro"; export { default as HLSPLayer } from "../src/components/HLSPLayer.astro";
export { default as Link } from "../src/components/Link.astro"; export { default as Link } from "../src/components/Link.astro";
@@ -44,6 +45,7 @@ export const Dropdown: any;
export const Field: any; export const Field: any;
export const FieldRow: any; export const FieldRow: any;
export const FileUploader: any; export const FileUploader: any;
export const FLVPLayer: any;
export const Form: any; export const Form: any;
export const HLSPLayer: any; export const HLSPLayer: any;
export const Link: any; export const Link: any;
+207
View File
@@ -0,0 +1,207 @@
---
import type { HTMLAttributes } from "astro/types";
import { cn } from "../utils/cn";
interface Props extends HTMLAttributes<"div"> {
videoUrl: string;
autoPlay?: boolean;
muted?: boolean;
volume?: number;
overlayInfo?: string;
poster?: string;
}
const {
videoUrl,
class: className = "",
autoPlay = true,
muted = true,
volume = 0.75,
overlayInfo = "Live Stream",
poster = "",
} = Astro.props;
const id = `vp_${Math.random().toString(36).slice(2)}`;
---
<div id={`${id}_card`} class={cn("video-card", className)}>
<div class="video-wrap">
<video
id={`${id}_video`}
preload="metadata"
playsinline
{poster}
autoplay={autoPlay}
muted={muted}></video>
<!-- overlays -->
<div id={`${id}_loading`} class="loading-overlay">
<div class="spinner"></div>
</div>
<div id={`${id}_placeholder`} class="placeholder-overlay">
<svg
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
style="opacity:.7"
>
<rect x="2" y="6" width="14" height="12" rx="2"></rect>
<path
d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"
></path>
</svg>
<div class="small">{overlayInfo}</div>
</div>
</div>
<!-- Controls -->
<div class="controls">
<div class="volume-wrap">
<button id={`${id}_volbtn`} class="btn" aria-label="Toggle mute"
>🔊</button
>
</div>
<button id={`${id}_fsbtn`} class="btn" aria-label="Fullscreen">⤢</button
>
</div>
</div>
<style>
.video-card {
position: relative;
width: 100%;
aspect-ratio: 16/9;
background: #000;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(2, 6, 23, 0.6);
}
video {
width: 100%;
height: 100%;
object-fit: contain;
}
.loading-overlay,
.placeholder-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
}
.loading-overlay {
display: none;
background: rgba(0, 0, 0, 0.6);
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.2);
border-top: 4px solid #fff;
border-radius: 50%;
width: 36px;
height: 36px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.6), transparent);
display: flex;
align-items: center;
justify-content: space-between;
z-index: 10;
}
</style>
<script is:inline define:vars={{ videoUrl, autoPlay, muted, volume, id }}>
document.addEventListener("DOMContentLoaded", () => {
const get = (suffix) => document.getElementById(`${id}_${suffix}`);
const card = get("card");
const video = get("video");
const loading = get("loading");
const placeholder = get("placeholder");
const volumeBtn = get("volbtn");
const fullscreenBtn = get("fsbtn");
const flvjs = window.flvjs;
// Load FLV or fallback
if (
(flvjs && flvjs.isSupported() && videoUrl.endsWith(".flv")) ||
videoUrl.includes(":8089/")
) {
const player = flvjs.createPlayer({
type: "flv",
isLive: true,
url: videoUrl,
});
player.attachMediaElement(video);
player.load();
player.play().catch(() => {
console.log(
"[INFO] Autoplay blocked, waiting for user gesture.",
);
});
} else {
video.src = videoUrl;
}
// Initial settings
video.volume = volume || 0.75;
video.muted = muted;
// Events
video.addEventListener(
"waiting",
() => (loading.style.display = "flex"),
);
video.addEventListener("canplay", () => {
loading.style.display = "none";
placeholder.style.display = "none";
});
const updateVolIcon = () => {
const val = video.muted ? 0 : Math.round(video.volume * 100);
volumeBtn.textContent = val === 0 ? "🔈" : val < 50 ? "🔉" : "🔊";
};
// --- Handle all mute/unmute logic robustly ---
volumeBtn.addEventListener("click", async () => {
try {
if (video.paused) await video.play().catch(() => {});
video.muted = !video.muted;
if (!video.muted) {
await video.play().catch(() => {});
}
updateVolIcon();
} catch (err) {
console.warn("[Volume] Toggle error:", err);
}
});
updateVolIcon();
// Fullscreen toggle
fullscreenBtn.addEventListener("click", async () => {
if (!document.fullscreenElement) await card.requestFullscreen();
else await document.exitFullscreen();
});
});
</script>
+2 -2
View File
@@ -4,7 +4,7 @@ import { cn } from "../utils/cn";
interface Props extends HTMLAttributes<"div"> { interface Props extends HTMLAttributes<"div"> {
videoUrl: string; videoUrl: string;
videoType: string; videoType?: string;
isLive?: boolean; isLive?: boolean;
autoPlay?: boolean; autoPlay?: boolean;
muted?: boolean; muted?: boolean;
@@ -15,7 +15,7 @@ interface Props extends HTMLAttributes<"div"> {
const { const {
videoUrl, videoUrl,
videoType, videoType = "application/x-mpegURL",
isLive = false, isLive = false,
class: className = "", class: className = "",
autoPlay = false, autoPlay = false,
+3 -1
View File
@@ -1,3 +1,4 @@
import type FlvJs from 'flv.js';
import Hls from 'hls.js'; import Hls from 'hls.js';
declare global { declare global {
@@ -7,7 +8,8 @@ declare global {
upload: any; upload: any;
myModal_open: any; myModal_open: any;
myModal_close: any; myModal_close: any;
hls: typeof Hls hls: typeof Hls,
flvjs: typeof FlvJs
} }
} }
+10 -1
View File
@@ -1,21 +1,30 @@
--- ---
import FLVPLayer from "../components/FLVPLayer.astro";
import HLSPLayer from "../components/HLSPLayer.astro"; import HLSPLayer from "../components/HLSPLayer.astro";
import ComponentLayout from "../layouts/ComponentLayout.astro"; import ComponentLayout from "../layouts/ComponentLayout.astro";
--- ---
<ComponentLayout> <ComponentLayout>
<HLSPLayer <!-- <HLSPLayer
videoUrl="https://stream-middleware.workroot.in/hls/live/t05Pbqv01N/2025-10-04_15-26-30/master.m3u8" videoUrl="https://stream-middleware.workroot.in/hls/live/t05Pbqv01N/2025-10-04_15-26-30/master.m3u8"
poster="https://stream-middleware.workroot.in/thumbnail/live/t05Pbqv01N/2025-10-04_15-26-30.jpg" poster="https://stream-middleware.workroot.in/thumbnail/live/t05Pbqv01N/2025-10-04_15-26-30.jpg"
videoType="application/x-mpegURL" videoType="application/x-mpegURL"
class="wr:w-[500px]" class="wr:w-[500px]"
muted={false} muted={false}
volume={0.7} volume={0.7}
/> -->
<FLVPLayer
videoUrl="http://127.0.0.1:8089/live/hIaBJegynP?token=p9HmND9I6r"
class="wr:w-[500px]"
muted={true}
volume={0.7}
/> />
</ComponentLayout> </ComponentLayout>
<script> <script>
import Hls from "hls.js"; import Hls from "hls.js";
import FlvJs from "flv.js";
window.hls = Hls; window.hls = Hls;
window.flvjs = FlvJs;
</script> </script>