diff --git a/demo/package.json b/demo/package.json index 87472192..0bb8ff88 100644 --- a/demo/package.json +++ b/demo/package.json @@ -17,9 +17,9 @@ "dplayer": "^1.27.1", "mux.js": "^6.3.0", "p2p-media-loader-core": "workspace:*", + "p2p-media-loader-demo": "workspace:*", "p2p-media-loader-hlsjs": "workspace:*", "p2p-media-loader-shaka": "workspace:*", - "p2p-media-loader-demo": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/demo/public/mejs-controls.svg b/demo/public/mejs-controls.svg new file mode 100644 index 00000000..db1938e4 --- /dev/null +++ b/demo/public/mejs-controls.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/p2p-media-loader-demo/package.json b/packages/p2p-media-loader-demo/package.json index fe4d1e20..b7592b0a 100644 --- a/packages/p2p-media-loader-demo/package.json +++ b/packages/p2p-media-loader-demo/package.json @@ -45,10 +45,17 @@ "clean-with-modules": "rimraf node_modules && pnpm clean" }, "dependencies": { + "@vidstack/react": "^1.11.21", "d3": "^7.9.0", + "dplayer": "^1.27.1", "hls.js": "^1.5.7", + "mediaelement": "^7.0.5", + "openplayerjs": "^2.14.3", "p2p-media-loader-core": "workspace:*", - "p2p-media-loader-hlsjs": "workspace:*" + "p2p-media-loader-hlsjs": "workspace:*", + "p2p-media-loader-shaka": "workspace:*", + "plyr": "^3.7.8", + "shaka-player": "^4.7.13" }, "devDependencies": { "@types/d3": "^7.4.3", diff --git a/packages/p2p-media-loader-demo/src/components/P2PVideoDemo.tsx b/packages/p2p-media-loader-demo/src/components/P2PVideoDemo.tsx index a32fb56c..825c01bf 100644 --- a/packages/p2p-media-loader-demo/src/components/P2PVideoDemo.tsx +++ b/packages/p2p-media-loader-demo/src/components/P2PVideoDemo.tsx @@ -1,29 +1,56 @@ import "./demo.css"; -import type Hls from "hls.js"; import { PlaybackOptions } from "./PlaybackOptions"; -import { PLAYERS } from "../constants"; +import { DEBUG_COMPONENT_ENABLED, PLAYERS } from "../constants"; import { useQueryParams } from "../hooks/useQueryParams"; -import { HlsjsPlayer } from "./players/Hlsjs"; +import { HlsjsPlayer } from "./players/hlsjs/Hlsjs"; import { useCallback, useMemo, useRef, useState } from "react"; import { DownloadStatsChart } from "./chart/DownloadStatsChart"; import { NodeNetwork } from "./nodeNetwork/NodeNetwork"; import { DebugTools } from "./debugTools/DebugTools"; -import { DownloadStats } from "../types"; +import { DownloadStats, PlayerKey } from "../types"; +import { HlsjsDPlayer } from "./players/hlsjs/HlsjsDPLayer"; +import { HlsjsClapprPlayer } from "./players/hlsjs/HlsjsClapprPlayer"; +import { HlsjsPlyr } from "./players/hlsjs/HlsjsPlyr"; +import { HlsjsOpenPlayer } from "./players/hlsjs/HlsjsOpenPlayer"; +import { Shaka } from "./players/shaka/Shaka"; +import { ShakaDPlayer } from "./players/shaka/ShakaDPlayer"; +import { ShakaClappr } from "./players/shaka/ShakaClappr"; +import { HlsjsMediaElement } from "./players/hlsjs/HlsjsMediaElement"; +import { ShakaPlyr } from "./players/shaka/ShakaPlyr"; +import { HlsJsP2PEngine } from "p2p-media-loader-hlsjs"; +import Hls from "hls.js"; +import { HlsjsVidstack } from "./players/hlsjs/HlsjsVidstack"; + +type DemoProps = { + debugToolsEnabled?: boolean; +}; + +const HlsWithP2PType = HlsJsP2PEngine.injectMixin(Hls); declare global { interface Window { - Hls: typeof Hls; - videoPlayer?: { destroy?: () => void }; + shaka?: unknown; + Hls?: typeof HlsWithP2PType; + LevelSelector: unknown; + DashShakaPlayback: unknown; } } -export type Player = (typeof PLAYERS)[number]; - -type DemoProps = { - debugToolsEnabled?: boolean; +const playerComponents = { + openPlayer_hls: HlsjsOpenPlayer, + plyr_hls: HlsjsPlyr, + clappr_hls: HlsjsClapprPlayer, + dplayer_hls: HlsjsDPlayer, + hlsjs_hls: HlsjsPlayer, + shaka: Shaka, + dplayer_shaka: ShakaDPlayer, + clappr_shaka: ShakaClappr, + mediaElement_hls: HlsjsMediaElement, + plyr_shaka: ShakaPlyr, + vidstack_hls: HlsjsVidstack, }; -export const P2PVideoDemo = ({ debugToolsEnabled }: DemoProps) => { +export const P2PVideoDemo = ({ debugToolsEnabled = false }: DemoProps) => { const data = useRef({ httpDownloaded: 0, p2pDownloaded: 0, @@ -72,26 +99,23 @@ export const P2PVideoDemo = ({ debugToolsEnabled }: DemoProps) => { }, []); const handlePlaybackOptionsUpdate = (url: string, player: string) => { - if (!PLAYERS.includes(player as Player)) return; + if (!(player in PLAYERS)) return; setURLQueryParams({ streamUrl: url, player }); }; const renderPlayer = () => { - switch (queryParams.player) { - case "hlsjs": - return ( - - ); - default: - return null; - } + const PlayerComponent = playerComponents[queryParams.player as PlayerKey]; + + return PlayerComponent ? ( + + ) : null; }; return ( @@ -125,7 +149,9 @@ export const P2PVideoDemo = ({ debugToolsEnabled }: DemoProps) => { )} - {debugToolsEnabled && } + {(debugToolsEnabled || queryParams.debug === DEBUG_COMPONENT_ENABLED) && ( + + )} ); }; diff --git a/packages/p2p-media-loader-demo/src/components/PlaybackOptions.tsx b/packages/p2p-media-loader-demo/src/components/PlaybackOptions.tsx index 9458d60a..0f825307 100644 --- a/packages/p2p-media-loader-demo/src/components/PlaybackOptions.tsx +++ b/packages/p2p-media-loader-demo/src/components/PlaybackOptions.tsx @@ -1,5 +1,6 @@ import { useRef } from "react"; import { PLAYERS } from "../constants"; +import { PlayerKey, PlayerName } from "../types"; type PlaybackOptions = { updatePlaybackOptions: (url: string, player: string) => void; @@ -17,6 +18,17 @@ export const PlaybackOptions = ({ const isHttps = window.location.protocol === "https:"; + const hlsPlayers: Partial> = {}; + const shakaPlayers: Partial> = {}; + + Object.entries(PLAYERS).forEach(([key, name]) => { + if (key.includes("hls")) { + hlsPlayers[key as PlayerKey] = name; + } else if (key.includes("shaka")) { + shakaPlayers[key as PlayerKey] = name; + } + }); + const handleApply = () => { const player = playerSelectRef.current?.value; const streamUrl = streamUrlInputRef.current?.value; @@ -34,6 +46,7 @@ export const PlaybackOptions = ({ Video URL{isHttps ? " (HTTPS only)" : ""}: Player: diff --git a/packages/p2p-media-loader-demo/src/components/debugTools/DebugTools.tsx b/packages/p2p-media-loader-demo/src/components/debugTools/DebugTools.tsx index 9c6aa56e..ee78b8b1 100644 --- a/packages/p2p-media-loader-demo/src/components/debugTools/DebugTools.tsx +++ b/packages/p2p-media-loader-demo/src/components/debugTools/DebugTools.tsx @@ -4,7 +4,6 @@ export const DebugTools = () => { return ( ); }; diff --git a/packages/p2p-media-loader-demo/src/components/demo.css b/packages/p2p-media-loader-demo/src/components/demo.css index e7827b89..5514f2ff 100644 --- a/packages/p2p-media-loader-demo/src/components/demo.css +++ b/packages/p2p-media-loader-demo/src/components/demo.css @@ -146,3 +146,12 @@ display: flex; gap: 1em; } + +.error-message { + margin-top: 1em; + padding: 1em; + background-color: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 0.25rem; + color: #721c24; +} diff --git a/packages/p2p-media-loader-demo/src/components/players/Hlsjs.tsx b/packages/p2p-media-loader-demo/src/components/players/Hlsjs.tsx deleted file mode 100644 index 2e886e60..00000000 --- a/packages/p2p-media-loader-demo/src/components/players/Hlsjs.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect, useRef } from "react"; -import { HlsJsP2PEngine } from "p2p-media-loader-hlsjs"; -import Hls from "hls.js"; - -type HlsjsPlayerProps = { - streamUrl: string; - announceTrackers: string[]; - onPeerConnect?: (peerId: string) => void; - onPeerDisconnect?: (peerId: string) => void; - onChunkDownloaded?: (bytesLength: number, downloadSource: string) => void; - onChunkUploaded?: (bytesLength: number) => void; -}; -const HlsWithP2P = HlsJsP2PEngine.injectMixin(Hls); - -export const HlsjsPlayer = ({ - streamUrl, - announceTrackers, - onPeerConnect, - onPeerDisconnect, - onChunkDownloaded, - onChunkUploaded, -}: HlsjsPlayerProps) => { - const videoRef = useRef(null); - - useEffect(() => { - if (!videoRef.current) return; - - const hls = new HlsWithP2P({ - p2p: { - core: { - announceTrackers, - }, - }, - }); - - if (onPeerConnect) { - hls.p2pEngine.addEventListener("onPeerConnect", onPeerConnect); - } - if (onPeerDisconnect) { - hls.p2pEngine.addEventListener("onPeerClose", onPeerDisconnect); - } - if (onChunkDownloaded) { - hls.p2pEngine.addEventListener("onChunkDownloaded", onChunkDownloaded); - } - if (onChunkUploaded) { - hls.p2pEngine.addEventListener("onChunkUploaded", onChunkUploaded); - } - - hls.attachMedia(videoRef.current); - hls.loadSource(streamUrl); - - return () => hls.destroy(); - }, [ - onPeerConnect, - onPeerDisconnect, - onChunkDownloaded, - onChunkUploaded, - streamUrl, - announceTrackers, - ]); - - return ( -
-
- ); -}; diff --git a/packages/p2p-media-loader-demo/src/components/players/clappr.css b/packages/p2p-media-loader-demo/src/components/players/clappr.css new file mode 100644 index 00000000..9159cc6e --- /dev/null +++ b/packages/p2p-media-loader-demo/src/components/players/clappr.css @@ -0,0 +1,49 @@ +#clappr-player { + width: 100%; + height: 100%; +} +@media (max-width: 376px) { + #clappr-player { + height: 200px; + } +} + +@media (max-width: 426px) { + #clappr-player { + height: 222px; + } +} + +@media (min-width: 427px) { + #clappr-player { + height: 304px; + } +} + +@media (min-width: 576px) { + #clappr-player { + height: 304px; + } +} + +@media (min-width: 768px) { + #clappr-player { + height: 253px; + } +} + +@media (min-width: 992px) { + #clappr-player { + height: 343px; + } +} + +@media (min-width: 1200px) { + #clappr-player { + height: 411px; + } +} + +#clappr-player { + width: 100%; +} diff --git a/packages/p2p-media-loader-demo/src/components/players/hlsjs/Hlsjs.css b/packages/p2p-media-loader-demo/src/components/players/hlsjs/Hlsjs.css new file mode 100644 index 00000000..ae4f0606 --- /dev/null +++ b/packages/p2p-media-loader-demo/src/components/players/hlsjs/Hlsjs.css @@ -0,0 +1,46 @@ +.select-container { + position: relative; + width: 150px; + margin-left: auto; +} + +.select-container select { + width: 100%; + padding: 8px 16px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: white; + font-size: 16px; + color: #333; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; +} + +.select-container:after { + content: "▼"; + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + color: #888; + pointer-events: none; + font-size: 12px; +} + +.select-container select:hover { + border-color: #888; + cursor: pointer; +} + +.select-container select:focus { + outline: none; + border-color: #555; +} + +.select-container select:disabled { + background-color: #eee; + color: #666; + cursor: not-allowed; +} diff --git a/packages/p2p-media-loader-demo/src/components/players/hlsjs/Hlsjs.tsx b/packages/p2p-media-loader-demo/src/components/players/hlsjs/Hlsjs.tsx new file mode 100644 index 00000000..c8bde705 --- /dev/null +++ b/packages/p2p-media-loader-demo/src/components/players/hlsjs/Hlsjs.tsx @@ -0,0 +1,96 @@ +import "./Hlsjs.css"; +import { useEffect, useRef, useState } from "react"; +import { PlayerProps } from "../../../types"; +import { subscribeToUiEvents } from "../utils"; +import { HlsJsP2PEngine } from "p2p-media-loader-hlsjs"; +import Hls from "hls.js"; + +export const HlsjsPlayer = ({ + streamUrl, + announceTrackers, + onPeerConnect, + onPeerDisconnect, + onChunkDownloaded, + onChunkUploaded, +}: PlayerProps) => { + const [isHlsSupported, setIsHlsSupported] = useState(true); + + const videoRef = useRef(null); + const qualityRef = useRef(null); + + useEffect(() => { + if (!Hls.isSupported()) { + setIsHlsSupported(false); + return; + } + + if (!videoRef.current) return; + + const HlsWithP2P = HlsJsP2PEngine.injectMixin(Hls); + const hls = new HlsWithP2P({ + p2p: { + core: { + swarmId: "custom swarm ID for stream 2000341", + announceTrackers, + }, + onHlsJsCreated(hls) { + subscribeToUiEvents({ + engine: hls.p2pEngine, + onPeerConnect, + onPeerDisconnect, + onChunkDownloaded, + onChunkUploaded, + }); + }, + }, + }); + + hls.attachMedia(videoRef.current); + hls.loadSource(streamUrl); + + hls.on(Hls.Events.MANIFEST_PARSED, () => { + if (!qualityRef.current) return; + updateQualityOptions(hls, qualityRef.current); + }); + + return () => hls.destroy(); + }, [ + onPeerConnect, + onPeerDisconnect, + onChunkDownloaded, + onChunkUploaded, + streamUrl, + announceTrackers, + ]); + + const updateQualityOptions = (hls: Hls, selectElement: HTMLSelectElement) => { + if (hls.levels.length < 2) { + selectElement.style.display = "none"; + } else { + selectElement.style.display = "block"; + selectElement.options.length = 0; + selectElement.add(new Option("Auto", "-1")); + hls.levels.forEach((level, index) => { + const label = `${level.height}p (${Math.round(level.bitrate / 1000)}k)`; + selectElement.add(new Option(label, index.toString())); + }); + + selectElement.addEventListener("change", () => { + hls.currentLevel = parseInt(selectElement.value); + }); + } + }; + + return isHlsSupported ? ( +
+