UNPKG

media-stream-player

Version:

Player built on top of media-stream-library

1,318 lines (1,279 loc) 83.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/index.ts var src_exports = {}; __export(src_exports, { AxisApi: () => AxisApi, BasicPlayer: () => BasicPlayer, Container: () => Container, FORMAT_API: () => FORMAT_API, Format: () => Format, Layer: () => Layer, PlaybackArea: () => PlaybackArea, Player: () => Player, Protocol: () => Protocol, Stats: () => Stats, browserSupportedFormats: () => browserSupportedFormats, getImageURL: () => getImageURL }); module.exports = __toCommonJS(src_exports); // src/MediaStreamPlayer.tsx var import_react26 = __toESM(require("react")); var import_client = require("react-dom/client"); // src/BasicPlayer.tsx var import_react21 = __toESM(require("react")); // src/Container.tsx var import_react = __toESM(require("react")); var import_styled_components = __toESM(require("styled-components")); var DEFAULT_ASPECT_RATIO = 16 / 9; var getHeightPct = (aspectRatio) => { if (aspectRatio === 0) { throw new Error("Cannot handle aspect ratio 0"); } return 100 / aspectRatio; }; var ContainerBody = import_styled_components.default.div.attrs( ({ aspectRatio }) => { return { style: { paddingTop: `${getHeightPct(aspectRatio)}%` } }; } )` width: 100%; background: black; position: relative; `; var Layer = import_styled_components.default.div` position: absolute; top: 0; left: 0; bottom: 0; right: 0; `; var Container = ({ aspectRatio = DEFAULT_ASPECT_RATIO, children }) => /* @__PURE__ */ import_react.default.createElement(ContainerBody, { aspectRatio }, children); // src/Controls.tsx var import_react13 = __toESM(require("react")); var import_luxon = require("luxon"); var import_styled_components5 = __toESM(require("styled-components")); // src/Settings.tsx var import_react3 = __toESM(require("react")); var import_styled_components3 = __toESM(require("styled-components")); // src/components/Switch.tsx var import_react2 = __toESM(require("react")); var import_styled_components2 = __toESM(require("styled-components")); var Container2 = import_styled_components2.default.label` position: relative; display: inline-block; width: 28px; height: 16px; `; var Input = import_styled_components2.default.input` opacity: 0; width: 0; height: 0; `; var Slider = import_styled_components2.default.span` border-radius: 16px; cursor: pointer; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: #ccc; transition: 0.4s; &:before { border-radius: 50%; content: ''; position: absolute; height: 12px; width: 12px; left: 2px; bottom: 2px; background-color: white; transition: 0.4s; } ${Input}:checked + & { background-color: #2196f3; } ${Input}:checked + &:before { transform: translateX(12px); } ${Input}:focus + & { box-shadow: 0 0 1px #2196f3; } `; var Switch = (props) => { return /* @__PURE__ */ import_react2.default.createElement(Container2, null, /* @__PURE__ */ import_react2.default.createElement(Input, __spreadValues({ type: "checkbox" }, props)), /* @__PURE__ */ import_react2.default.createElement(Slider, null)); }; // src/Settings.tsx var SettingsMenu = import_styled_components3.default.div` font-family: sans-serif; display: flex; flex-direction: column; position: absolute; bottom: 32px; right: 0; background: rgb(0, 0, 0, 0.66); padding: 8px 16px; margin-bottom: 16px; margin-right: 8px; &:after { content: ''; width: 10px; height: 10px; transform: rotate(45deg); position: absolute; bottom: -5px; right: 12px; background: rgb(0, 0, 0, 0.66); } `; var SettingsItem = import_styled_components3.default.div` display: flex; flex-direction: row; color: white; height: 24px; width: 320px; align-items: center; justify-content: space-between; margin: 4px 0; `; var Settings = ({ parameters, format, onFormat, onVapix, showStatsOverlay, toggleStats }) => { const [textString, setTextString] = (0, import_react3.useState)(parameters["textstring"]); const textStringTimeout = (0, import_react3.useRef)(); const changeParam = (0, import_react3.useCallback)( (e) => { const { name, value } = e.target; switch (name) { case "textstring": setTextString(value); clearTimeout(textStringTimeout.current); textStringTimeout.current = window.setTimeout(() => { onVapix(name, value); }, 300); break; case "text": onVapix(name, value ? "1" : "0"); break; default: console.warn("internal error"); } }, [onVapix] ); const changeStatsOverlay = (0, import_react3.useCallback)( (e) => toggleStats(e.target.checked), [toggleStats] ); const changeFormat = (0, import_react3.useCallback)( (e) => onFormat(e.target.value), [onFormat] ); const changeResolution = (0, import_react3.useCallback)( (e) => onVapix("resolution", e.target.value), [onVapix] ); const changeRotation = (0, import_react3.useCallback)( (e) => onVapix("rotation", e.target.value), [onVapix] ); const changeCompression = (0, import_react3.useCallback)( (e) => onVapix("compression", e.target.value), [onVapix] ); return /* @__PURE__ */ import_react3.default.createElement(SettingsMenu, null, /* @__PURE__ */ import_react3.default.createElement(SettingsItem, null, /* @__PURE__ */ import_react3.default.createElement("div", null, "Format"), /* @__PURE__ */ import_react3.default.createElement("select", { onChange: changeFormat, defaultValue: format }, /* @__PURE__ */ import_react3.default.createElement("option", { value: "RTP_H264" }, "H.264 (RTP over WS)"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "MP4_H264" }, "H.264 (MP4 over HTTP)"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "RTP_JPEG" }, "Motion JPEG"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "JPEG" }, "Still image"))), /* @__PURE__ */ import_react3.default.createElement(SettingsItem, null, /* @__PURE__ */ import_react3.default.createElement("div", null, "Resolution"), /* @__PURE__ */ import_react3.default.createElement("select", { value: parameters["resolution"], onChange: changeResolution }, /* @__PURE__ */ import_react3.default.createElement("option", { value: "" }, "default"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "1920x1080" }, "1920 x 1080 (FHD)"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "1280x720" }, "1280 x 720 (HD)"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "800x600" }, "800 x 600 (VGA)"))), /* @__PURE__ */ import_react3.default.createElement(SettingsItem, null, /* @__PURE__ */ import_react3.default.createElement("div", null, "Rotation"), /* @__PURE__ */ import_react3.default.createElement("select", { value: parameters["rotation"], onChange: changeRotation }, /* @__PURE__ */ import_react3.default.createElement("option", { value: "0" }, "0"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "90" }, "90"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "180" }, "180"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "270" }, "270"))), /* @__PURE__ */ import_react3.default.createElement(SettingsItem, null, /* @__PURE__ */ import_react3.default.createElement("div", null, "Compression"), /* @__PURE__ */ import_react3.default.createElement("select", { value: parameters["compression"], onChange: changeCompression }, /* @__PURE__ */ import_react3.default.createElement("option", { value: "" }, "default"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "0" }, "0"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "10" }, "10"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "20" }, "20"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "30" }, "30"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "40" }, "40"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "50" }, "50"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "60" }, "60"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "70" }, "70"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "80" }, "80"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "90" }, "90"), /* @__PURE__ */ import_react3.default.createElement("option", { value: "100" }, "100"))), /* @__PURE__ */ import_react3.default.createElement(SettingsItem, null, /* @__PURE__ */ import_react3.default.createElement("div", null, "Text overlay"), /* @__PURE__ */ import_react3.default.createElement("input", { name: "textstring", value: textString, onChange: changeParam }), /* @__PURE__ */ import_react3.default.createElement( Switch, { name: "text", checked: parameters["text"] === "1", onChange: changeParam } )), /* @__PURE__ */ import_react3.default.createElement(SettingsItem, null, /* @__PURE__ */ import_react3.default.createElement("div", null, "Stats overlay"), /* @__PURE__ */ import_react3.default.createElement(Switch, { checked: showStatsOverlay, onChange: changeStatsOverlay }))); }; // src/components/Button.tsx var import_styled_components4 = __toESM(require("styled-components")); var Button = import_styled_components4.default.button` background: transparent; fill: white; border: none; box-sizing: border-box; padding: 0; margin: 0; line-height: 0; :focus { outline: none; } `; // src/hooks/useUserActive.ts var import_react4 = require("react"); var DEFAULT_TIMEOUT = 3e3; var useUserActive = (ref, duration = DEFAULT_TIMEOUT) => { const [userActive, setUserActive] = (0, import_react4.useState)(false); const startUserActive = () => setUserActive(true); const stopUserActive = () => setUserActive(false); (0, import_react4.useEffect)(() => { if (userActive) { const timer = setTimeout(stopUserActive, duration); return () => { clearTimeout(timer); }; } }); (0, import_react4.useEffect)(() => { const el = ref.current; if (el === null) { return; } el.addEventListener("pointermove", startUserActive); if (userActive) { el.addEventListener("pointerleave", stopUserActive); } return () => { el.removeEventListener("pointermove", startUserActive); if (userActive) { el.removeEventListener("pointerleave", stopUserActive); } }; }, [userActive, ref]); return userActive; }; // src/img/CogWheel.tsx var import_react5 = __toESM(require("react")); var CogWheel = ({ title }) => { return /* @__PURE__ */ import_react5.default.createElement( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 20 20" }, title !== void 0 ? /* @__PURE__ */ import_react5.default.createElement("title", null, title) : null, /* @__PURE__ */ import_react5.default.createElement("path", { fill: "none", d: "M0 0h20v20H0V0z" }), /* @__PURE__ */ import_react5.default.createElement("path", { d: "M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z" }) ); }; // src/img/Pause.tsx var import_react6 = __toESM(require("react")); var Pause = ({ title }) => { return /* @__PURE__ */ import_react6.default.createElement( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24" }, title !== void 0 ? /* @__PURE__ */ import_react6.default.createElement("title", null, title) : null, /* @__PURE__ */ import_react6.default.createElement("path", { d: "M6 19h4V5H6v14zm8-14v14h4V5h-4z" }), /* @__PURE__ */ import_react6.default.createElement("path", { d: "M0 0h24v24H0z", fill: "none" }) ); }; // src/img/Play.tsx var import_react7 = __toESM(require("react")); var Play = ({ title }) => { return /* @__PURE__ */ import_react7.default.createElement( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24" }, title !== void 0 ? /* @__PURE__ */ import_react7.default.createElement("title", null, title) : null, /* @__PURE__ */ import_react7.default.createElement("path", { d: "M8 5v14l11-7z" }), /* @__PURE__ */ import_react7.default.createElement("path", { d: "M0 0h24v24H0z", fill: "none" }) ); }; // src/img/Refresh.tsx var import_react8 = __toESM(require("react")); var Refresh = ({ title }) => { return /* @__PURE__ */ import_react8.default.createElement( "svg", { xmlns: "http://www.w3.org/2000/svg", height: "24", viewBox: "0 0 24 24", width: "24" }, title !== void 0 ? /* @__PURE__ */ import_react8.default.createElement("title", null, title) : null, /* @__PURE__ */ import_react8.default.createElement("path", { d: "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" }), /* @__PURE__ */ import_react8.default.createElement("path", { d: "M0 0h24v24H0z", fill: "none" }) ); }; // src/img/Screenshot.tsx var import_react9 = __toESM(require("react")); var Screenshot = ({ title }) => { return /* @__PURE__ */ import_react9.default.createElement( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24" }, title !== void 0 ? /* @__PURE__ */ import_react9.default.createElement("title", null, title) : null, /* @__PURE__ */ import_react9.default.createElement("path", { d: "M0 0h24v24H0z", fill: "none" }), /* @__PURE__ */ import_react9.default.createElement("circle", { cx: "12", cy: "12", r: "3.2" }), /* @__PURE__ */ import_react9.default.createElement("path", { d: "M9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z" }) ); }; // src/img/Spinner.tsx var import_react10 = __toESM(require("react")); var Spinner = () => { return /* @__PURE__ */ import_react10.default.createElement( "svg", { width: "38", height: "38", viewBox: "0 0 38 38", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ import_react10.default.createElement("defs", null, /* @__PURE__ */ import_react10.default.createElement("linearGradient", { x1: "8.042%", y1: "0%", x2: "65.682%", y2: "23.865%", id: "a" }, /* @__PURE__ */ import_react10.default.createElement("stop", { stopColor: "#fff", stopOpacity: "0", offset: "0%" }), /* @__PURE__ */ import_react10.default.createElement("stop", { stopColor: "#fff", stopOpacity: ".631", offset: "63.146%" }), /* @__PURE__ */ import_react10.default.createElement("stop", { stopColor: "#fff", offset: "100%" }))), /* @__PURE__ */ import_react10.default.createElement("g", { fill: "none", fillRule: "evenodd" }, /* @__PURE__ */ import_react10.default.createElement("g", { transform: "translate(1 1)" }, /* @__PURE__ */ import_react10.default.createElement( "path", { d: "M36 18c0-9.94-8.06-18-18-18", id: "Oval-2", stroke: "url(#a)", strokeWidth: "2" }, /* @__PURE__ */ import_react10.default.createElement( "animateTransform", { attributeName: "transform", type: "rotate", from: "0 18 18", to: "360 18 18", dur: "0.9s", repeatCount: "indefinite" } ) ), /* @__PURE__ */ import_react10.default.createElement("circle", { fill: "#fff", cx: "36", cy: "18", r: "1" }, /* @__PURE__ */ import_react10.default.createElement( "animateTransform", { attributeName: "transform", type: "rotate", from: "0 18 18", to: "360 18 18", dur: "0.9s", repeatCount: "indefinite" } )))) ); }; // src/img/Stop.tsx var import_react11 = __toESM(require("react")); var Stop = ({ title }) => { return /* @__PURE__ */ import_react11.default.createElement( "svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24" }, title !== void 0 ? /* @__PURE__ */ import_react11.default.createElement("title", null, title) : null, /* @__PURE__ */ import_react11.default.createElement("path", { d: "M0 0h24v24H0z", fill: "none" }), /* @__PURE__ */ import_react11.default.createElement("path", { d: "M6 6h12v12H6z" }) ); }; // src/img/StreamStats.tsx var import_react12 = __toESM(require("react")); var StreamStats = ({ title }) => { return /* @__PURE__ */ import_react12.default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24" }, title !== void 0 ? /* @__PURE__ */ import_react12.default.createElement("title", null, title) : null, /* @__PURE__ */ import_react12.default.createElement("path", { d: "M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z" })); }; // src/Controls.tsx function isHTMLMediaElement(el) { return el.buffered !== void 0; } var ControlArea = import_styled_components5.default.div` width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: flex-end; opacity: ${({ visible }) => visible ? 1 : 0}; transition: opacity 0.3s ease-in-out; `; var ControlBar = import_styled_components5.default.div` width: 100%; height: 32px; background: rgb(0, 0, 0, 0.66); display: flex; align-items: center; padding: 0 16px; box-sizing: border-box; `; var VolumeContainer = import_styled_components5.default.div` margin-left: 8px; `; var Progress = import_styled_components5.default.div` flex-grow: 2; padding: 0 32px; display: flex; align-items: center; `; var ProgressBarContainer = import_styled_components5.default.div` margin: 0; width: 100%; height: 24px; position: relative; display: flex; flex-direction: column; justify-content: center; `; var ProgressBar = import_styled_components5.default.div` background-color: rgba(255, 255, 255, 0.1); height: 1px; position: relative; width: 100%; ${ProgressBarContainer}:hover > & { height: 3px; } `; var ProgressBarPlayed = import_styled_components5.default.div.attrs( ({ fraction }) => { return { style: { transform: `scaleX(${fraction})` } }; } )` background-color: rgb(240, 180, 0); height: 100%; position: absolute; top: 0; transform: scaleX(0); transform-origin: 0 0; width: 100%; `; var ProgressBarBuffered = import_styled_components5.default.div.attrs( ({ fraction }) => { return { style: { transform: `scaleX(${fraction})` } }; } )` background-color: rgba(255, 255, 255, 0.2); height: 100%; position: absolute; top: 0; transform: scaleX(0); transform-origin: 0 0; width: 100%; `; var ProgressTimestamp = import_styled_components5.default.div.attrs( ({ left }) => { return { style: { left: `${left}px` } }; } )` background-color: rgb(56, 55, 51); border-radius: 3px; bottom: 200%; color: #fff; font-size: 9px; padding: 5px; position: absolute; text-align: center; `; var ProgressIndicator = import_styled_components5.default.div` color: rgb(240, 180, 0); padding-left: 24px; font-size: 10px; white-space: nowrap; `; var Controls = ({ play, videoProperties, duration, startTime, src, parameters, onPlay, onStop, onRefresh, onSeek, onScreenshot, onFormat, onVapix, labels, showStatsOverlay, toggleStats, format, volume, setVolume }) => { const controlArea = (0, import_react13.useRef)(null); const userActive = useUserActive(controlArea); const [settings, setSettings] = (0, import_react13.useState)(false); const toggleSettings = (0, import_react13.useCallback)( () => setSettings((currentSettings) => !currentSettings), [setSettings] ); const onVolumeChange = (0, import_react13.useCallback)( (e) => { if (setVolume !== void 0) { setVolume(parseFloat(e.target.value)); } }, [setVolume] ); const [totalDuration, setTotalDuration] = (0, import_react13.useState)(duration); const __mediaTimeline = (0, import_react13.useRef)({ startDateTime: startTime !== void 0 ? import_luxon.DateTime.fromISO(startTime) : void 0 }); const [progress, setProgress] = (0, import_react13.useState)({ playedFraction: 0, bufferedFraction: 0, counter: "" }); (0, import_react13.useEffect)(() => { var _a; if (videoProperties === void 0) { return; } const { el, pipeline, range } = videoProperties; if (el === null || pipeline === void 0) { return; } const [start = 0, end = duration] = range != null ? range : [0, duration]; const __duration = (_a = duration != null ? duration : end) != null ? _a : Infinity; setTotalDuration(__duration); const updateProgress = () => { const played = start + pipeline.currentTime; const buffered = isHTMLMediaElement(el) && el.buffered.length > 0 ? start + el.buffered.end(el.buffered.length - 1) : played; const total = __duration === Infinity ? buffered : __duration; const counter = `${import_luxon.Duration.fromMillis(played * 1e3).toFormat( "h:mm:ss" )} / ${import_luxon.Duration.fromMillis(total * 1e3).toFormat("h:mm:ss")}`; setProgress({ playedFraction: played / total, bufferedFraction: buffered / total, counter }); }; updateProgress(); if (isHTMLMediaElement(el)) { el.addEventListener("ended", updateProgress); el.addEventListener("progress", updateProgress); el.addEventListener("timeupdate", updateProgress); return () => { el.removeEventListener("timeupdate", updateProgress); el.removeEventListener("progress", updateProgress); el.removeEventListener("ended", updateProgress); }; } const progressInterval = setInterval(updateProgress, 1e3); return () => { clearInterval(progressInterval); }; }, [videoProperties, duration, startTime, setTotalDuration]); const seek = (0, import_react13.useCallback)( (e) => { if (totalDuration === void 0) { return; } const { left, width } = e.currentTarget.getBoundingClientRect(); const fraction = (e.pageX - left) / width; onSeek(fraction * totalDuration); }, [totalDuration, onSeek] ); const [timestamp, setTimestamp] = (0, import_react13.useState)({ left: 0, label: "" }); const __progressBarContainerRef = (0, import_react13.useRef)(null); (0, import_react13.useEffect)(() => { if (startTime !== void 0) { __mediaTimeline.current.startDateTime = import_luxon.DateTime.fromISO(startTime); } const el = __progressBarContainerRef.current; if (el === null || totalDuration === void 0) { return; } const { left, width } = el.getBoundingClientRect(); const showTimestamp = (e) => { const offset = e.pageX - left; const offsetMillis = offset / width * totalDuration * 1e3; setTimestamp({ left: offset, label: __mediaTimeline.current.startDateTime !== void 0 ? __mediaTimeline.current.startDateTime.plus(offsetMillis).toLocaleString(import_luxon.DateTime.DATETIME_FULL_WITH_SECONDS) : import_luxon.Duration.fromMillis(offsetMillis).toFormat("h:mm:ss") }); }; const start = () => { el.addEventListener("pointermove", showTimestamp); }; const stop = () => { setTimestamp({ left: 0, label: "" }); el.removeEventListener("pointermove", showTimestamp); }; el.addEventListener("pointerover", start); el.addEventListener("pointerout", stop); return () => { el.removeEventListener("pointerout", stop); el.removeEventListener("pointerover", start); }; }, [startTime, totalDuration]); return /* @__PURE__ */ import_react13.default.createElement( ControlArea, { ref: controlArea, visible: play !== true || settings || userActive }, /* @__PURE__ */ import_react13.default.createElement(ControlBar, null, /* @__PURE__ */ import_react13.default.createElement(Button, { onClick: onPlay }, play === true ? /* @__PURE__ */ import_react13.default.createElement(Pause, { title: labels == null ? void 0 : labels.pause }) : /* @__PURE__ */ import_react13.default.createElement(Play, { title: labels == null ? void 0 : labels.play })), src !== void 0 && /* @__PURE__ */ import_react13.default.createElement(Button, { onClick: onStop }, /* @__PURE__ */ import_react13.default.createElement(Stop, { title: labels == null ? void 0 : labels.stop })), src !== void 0 && /* @__PURE__ */ import_react13.default.createElement(Button, { onClick: onRefresh }, /* @__PURE__ */ import_react13.default.createElement(Refresh, { title: labels == null ? void 0 : labels.refresh })), src !== void 0 && /* @__PURE__ */ import_react13.default.createElement(Button, { onClick: onScreenshot }, /* @__PURE__ */ import_react13.default.createElement(Screenshot, { title: labels == null ? void 0 : labels.screenshot })), volume !== void 0 ? /* @__PURE__ */ import_react13.default.createElement(VolumeContainer, { title: labels == null ? void 0 : labels.volume }, /* @__PURE__ */ import_react13.default.createElement( "input", { type: "range", min: "0", max: "1", step: "0.05", onChange: onVolumeChange, value: volume != null ? volume : 0 } )) : null, /* @__PURE__ */ import_react13.default.createElement(Progress, null, /* @__PURE__ */ import_react13.default.createElement(ProgressBarContainer, { onClick: seek, ref: __progressBarContainerRef }, /* @__PURE__ */ import_react13.default.createElement(ProgressBar, null, /* @__PURE__ */ import_react13.default.createElement(ProgressBarPlayed, { fraction: progress.playedFraction }), /* @__PURE__ */ import_react13.default.createElement(ProgressBarBuffered, { fraction: progress.bufferedFraction }), timestamp.left !== 0 ? /* @__PURE__ */ import_react13.default.createElement(ProgressTimestamp, { left: timestamp.left }, timestamp.label) : null)), /* @__PURE__ */ import_react13.default.createElement(ProgressIndicator, null, totalDuration === Infinity ? "\u2219 LIVE" : progress.counter)), /* @__PURE__ */ import_react13.default.createElement(Button, { onClick: toggleSettings }, /* @__PURE__ */ import_react13.default.createElement(CogWheel, { title: labels == null ? void 0 : labels.settings }))), settings && /* @__PURE__ */ import_react13.default.createElement( Settings, { parameters, format, onFormat, onVapix, showStatsOverlay, toggleStats } ) ); }; // src/PlaybackArea.tsx var import_react20 = __toESM(require("react")); var import_debug5 = __toESM(require("debug")); // src/HttpMp4Video.tsx var import_react16 = __toESM(require("react")); var import_debug = __toESM(require("debug")); var import_media_stream_library = require("media-stream-library"); var import_styled_components6 = __toESM(require("styled-components")); // src/constants.ts var FORMAT_SUPPORTS_AUDIO = { RTP_H264: true, RTP_JPEG: false, MP4_H264: true, JPEG: false, MJPEG: false }; // src/hooks/useEventState.ts var import_react14 = require("react"); var useEventState = (ref, eventName) => { const [eventState, setEventState] = (0, import_react14.useState)(false); const setEventStateTrue = (0, import_react14.useCallback)(() => setEventState(true), []); const setEventStateFalse = (0, import_react14.useCallback)(() => setEventState(false), []); (0, import_react14.useEffect)(() => { const el = ref.current; if (!eventState && el !== null) { el.addEventListener(eventName, setEventStateTrue); return () => { el.removeEventListener(eventName, setEventStateTrue); }; } }, [eventState, eventName, ref, setEventStateTrue]); return [eventState, setEventStateFalse]; }; // src/hooks/useVideoDebug.ts var import_react15 = require("react"); var useVideoDebug = (videoEl, debugLog6) => { (0, import_react15.useEffect)(() => { if (videoEl === null) { return; } const onUpdate = () => { try { const currentTime = videoEl.currentTime; const bufferedEnd = videoEl.buffered.end(videoEl.buffered.length - 1); debugLog6("%o", { delay: bufferedEnd - currentTime, currentTime, bufferedEnd }); } catch (err) { debugLog6("%o", err); } }; videoEl.addEventListener("timeupdate", onUpdate); videoEl.addEventListener("progress", onUpdate); return () => { videoEl.removeEventListener("timeupdate", onUpdate); videoEl.removeEventListener("progress", onUpdate); }; }, [debugLog6, videoEl]); }; // src/types.ts var Format = /* @__PURE__ */ ((Format2) => { Format2["RTP_H264"] = "RTP_H264"; Format2["RTP_JPEG"] = "RTP_JPEG"; Format2["JPEG"] = "JPEG"; Format2["MJPEG"] = "MJPEG"; Format2["MP4_H264"] = "MP4_H264"; return Format2; })(Format || {}); // src/HttpMp4Video.tsx var debugLog = (0, import_debug.default)("msp:http-mp4-video"); var VideoNative = import_styled_components6.default.video` max-height: 100%; object-fit: contain; width: 100%; `; var HttpMp4Video = ({ forwardedRef, play = false, src, autoPlay = true, muted = true, onPlaying, onEnded, metadataHandler }) => { let videoRef = (0, import_react16.useRef)(null); if (typeof forwardedRef === "function") { forwardedRef(videoRef.current); } else if (forwardedRef) { videoRef = forwardedRef; } const [canplay, unsetCanplay] = useEventState(videoRef, "canplay"); const [playing, unsetPlaying] = useEventState(videoRef, "playing"); const [pipeline, setPipeline] = (0, import_react16.useState)( null ); const [fetching, setFetching] = (0, import_react16.useState)(false); const __onPlayingRef = (0, import_react16.useRef)(onPlaying); __onPlayingRef.current = onPlaying; const __onEndedRef = (0, import_react16.useRef)(onEnded); __onEndedRef.current = onEnded; const __sensorTmRef = (0, import_react16.useRef)(); useVideoDebug(videoRef.current, debugLog); (0, import_react16.useEffect)(() => { const videoEl = videoRef.current; if (videoEl === null) { return; } if (play && canplay === true && playing === false) { debugLog("play"); videoEl.play().catch((err) => { console.error("VideoElement error: ", err.message); }); const { videoHeight, videoWidth } = videoEl; debugLog("%o", { videoHeight, videoWidth }); } else if (!play && playing === true) { debugLog("pause"); videoEl.pause(); unsetPlaying(); } else if (play && playing === true) { if (__onPlayingRef.current !== void 0) { __onPlayingRef.current({ el: videoEl, width: videoEl.videoWidth, height: videoEl.videoHeight, sensorTm: __sensorTmRef.current, formatSupportsAudio: FORMAT_SUPPORTS_AUDIO["MP4_H264" /* MP4_H264 */] // TODO: no volume, need to expose tracks? // TODO: no pipeline, can we even get stats? }); } } }, [play, canplay, playing, unsetPlaying, pipeline]); const __metadataHandlerRef = (0, import_react16.useRef)(metadataHandler); __metadataHandlerRef.current = metadataHandler; (0, import_react16.useEffect)(() => { const videoEl = videoRef.current; if (src !== void 0 && src.length > 0 && videoEl !== null) { const endedCallback = () => { var _a; (_a = __onEndedRef.current) == null ? void 0 : _a.call(__onEndedRef); }; debugLog("create pipeline", src); const newPipeline = new import_media_stream_library.pipelines.HttpMsePipeline({ http: { uri: src }, mediaElement: videoEl }); setPipeline(newPipeline); newPipeline.onServerClose = endedCallback; return () => { debugLog("close pipeline and clear video"); newPipeline.close(); videoEl.src = ""; setPipeline(null); setFetching(false); unsetCanplay(); unsetPlaying(); }; } }, [src, unsetCanplay, unsetPlaying]); (0, import_react16.useEffect)(() => { if (play && pipeline && !fetching) { pipeline.onHeaders = (headers) => { var _a; __sensorTmRef.current = parseTransformHeader( (_a = headers.get("video-sensor-transform")) != null ? _a : headers.get("video-metadata-transform") ); }; pipeline.http.play(); debugLog("initiated data fetching"); setFetching(true); } }, [play, pipeline, fetching]); return /* @__PURE__ */ import_react16.default.createElement(VideoNative, { autoPlay, muted, ref: videoRef }); }; var parseTransformHeader = (value) => { if (value === void 0 || value === null) { return void 0; } return value.split(";").map((row) => row.split(",").map(Number)); }; // src/StillImage.tsx var import_react17 = __toESM(require("react")); var import_debug2 = __toESM(require("debug")); var import_styled_components7 = __toESM(require("styled-components")); var debugLog2 = (0, import_debug2.default)("msp:still-image"); var ImageNative = import_styled_components7.default.img` max-height: 100%; object-fit: contain; width: 100%; `; var cachebust = 0; var StillImage = ({ forwardedRef, play = false, onPlaying, src }) => { let imgRef = (0, import_react17.useRef)(null); if (typeof forwardedRef === "function") { forwardedRef(imgRef.current); } else if (forwardedRef) { imgRef = forwardedRef; } const [loaded, unsetLoaded] = useEventState(imgRef, "load"); (0, import_react17.useEffect)(() => { const imgEl = imgRef.current; if (imgEl === null) { return; } if (play && src !== void 0) { imgEl.src = `${src}&cachebust=${cachebust++}`; return () => { imgEl.src = ""; unsetLoaded(); }; } }, [play, src, unsetLoaded]); const __onPlayingRef = (0, import_react17.useRef)(onPlaying); __onPlayingRef.current = onPlaying; (0, import_react17.useEffect)(() => { const el = imgRef.current; if (loaded && el !== null && __onPlayingRef.current !== void 0) { __onPlayingRef.current({ el, width: el.naturalWidth, height: el.naturalHeight, formatSupportsAudio: FORMAT_SUPPORTS_AUDIO["JPEG" /* JPEG */] }); } }, [loaded]); debugLog2("render image", loaded); return /* @__PURE__ */ import_react17.default.createElement(ImageNative, { ref: imgRef }); }; // src/WsRtspCanvas.tsx var import_react18 = __toESM(require("react")); var import_debug3 = __toESM(require("debug")); var import_media_stream_library2 = require("media-stream-library"); var import_styled_components8 = __toESM(require("styled-components")); var debugLog3 = (0, import_debug3.default)("msp:ws-rtsp-video"); var CanvasNative = import_styled_components8.default.canvas` max-height: 100%; object-fit: contain; width: 100%; `; var WsRtspCanvas = ({ forwardedRef, play = true, ws = "", rtsp = "", onPlaying, onEnded, onSdp, onRtcp, offset = 0, autoRetry = false }) => { let canvasRef = (0, import_react18.useRef)(null); if (typeof forwardedRef === "function") { forwardedRef(canvasRef.current); } else if (forwardedRef) { canvasRef = forwardedRef; } const [pipeline, setPipeline] = (0, import_react18.useState)(null); const [fetching, setFetching] = (0, import_react18.useState)(false); const __offsetRef = (0, import_react18.useRef)(offset); const __rangeRef = (0, import_react18.useRef)([offset, void 0]); const timeout = (0, import_react18.useRef)(void 0); (0, import_react18.useEffect)(() => { if (pipeline === null) { return; } timeout.current = window.setInterval(() => { const { currentTime } = pipeline; debugLog3("%o", { currentTime }); }, 1e3); return () => window.clearTimeout(timeout.current); }, [pipeline]); (0, import_react18.useEffect)(() => { __offsetRef.current = offset; const canvas = canvasRef.current; if (ws && rtsp && canvas) { debugLog3("create pipeline"); const newPipeline = new import_media_stream_library2.pipelines.Html5CanvasPipeline({ ws: { uri: ws }, rtsp: { uri: rtsp }, mediaElement: canvas }); if (autoRetry) { import_media_stream_library2.utils.addRTSPRetry(newPipeline.rtsp); } setPipeline(newPipeline); return () => { debugLog3("destroy pipeline"); newPipeline.pause(); newPipeline.close(); setPipeline(null); setFetching(false); debugLog3("canvas cleared"); }; } }, [ws, rtsp, offset, autoRetry]); const __onPlayingRef = (0, import_react18.useRef)(onPlaying); __onPlayingRef.current = onPlaying; const __onEndedRef = (0, import_react18.useRef)(onEnded); __onEndedRef.current = onEnded; const __onSdpRef = (0, import_react18.useRef)(onSdp); __onSdpRef.current = onSdp; const __onRtcpRef = (0, import_react18.useRef)(onRtcp); __onRtcpRef.current = onRtcp; const __sensorTmRef = (0, import_react18.useRef)(); (0, import_react18.useEffect)(() => { if (play && pipeline && !fetching) { pipeline.ready.then(() => { debugLog3("fetch"); pipeline.onSdp = (sdp) => { var _a; const videoMedia = sdp.media.find((m) => { return m.type === "video"; }); if (videoMedia !== void 0) { __sensorTmRef.current = (_a = videoMedia["x-sensor-transform"]) != null ? _a : videoMedia["transform"]; } if (__onSdpRef.current !== void 0) { __onSdpRef.current(sdp); } }; pipeline.rtsp.onRtcp = (rtcp) => { var _a, _b; (_a = __onRtcpRef.current) == null ? void 0 : _a.call(__onRtcpRef, rtcp); if ((0, import_media_stream_library2.isRtcpBye)(rtcp)) { (_b = __onEndedRef.current) == null ? void 0 : _b.call(__onEndedRef); } }; pipeline.rtsp.onPlay = (range) => { if (range !== void 0) { __rangeRef.current = [ parseFloat(range[0]) || 0, parseFloat(range[1]) || void 0 ]; } }; pipeline.rtsp.play(__offsetRef.current); setFetching(true); }).catch(console.error); } else if (play && pipeline !== null) { debugLog3("play"); pipeline.play(); pipeline.onCanplay = () => { if (canvasRef.current !== null && __onPlayingRef.current !== void 0) { __onPlayingRef.current({ el: canvasRef.current, width: canvasRef.current.width, height: canvasRef.current.height, formatSupportsAudio: FORMAT_SUPPORTS_AUDIO["RTP_JPEG" /* RTP_JPEG */], range: __rangeRef.current, sensorTm: __sensorTmRef.current }); } }; } else if (!play && pipeline) { debugLog3("pause"); pipeline.pause(); } }, [play, pipeline, fetching]); return /* @__PURE__ */ import_react18.default.createElement(CanvasNative, { ref: canvasRef }); }; // src/WsRtspVideo.tsx var import_react19 = __toESM(require("react")); var import_debug4 = __toESM(require("debug")); var import_media_stream_library4 = require("media-stream-library"); var import_styled_components9 = __toESM(require("styled-components")); // src/metadata.ts var import_media_stream_library3 = require("media-stream-library"); var attachMetadataHandler = (pipeline, { parser, cb }) => { const scheduler = new import_media_stream_library3.utils.Scheduler(pipeline, cb, 30); const xmlParser = new DOMParser(); const xmlMessageHandler = (msg) => { const xmlDocument = xmlParser.parseFromString( msg.data.toString(), "text/xml" ); const newMsg = parser(__spreadProps(__spreadValues({}, msg), { xmlDocument })); if (msg.ntpTimestamp !== void 0) { scheduler.run(newMsg); } }; const onvifDepay = new import_media_stream_library3.components.ONVIFDepay(); const onvifHandlerPipe = import_media_stream_library3.components.Tube.fromHandlers((msg) => { if (msg.type === import_media_stream_library3.MessageType.XML) { xmlMessageHandler(msg); } }, void 0); pipeline.insertAfter(pipeline.rtsp, onvifDepay); pipeline.insertAfter(onvifDepay, onvifHandlerPipe); pipeline.onSync = (ntpPresentationTime) => scheduler.init(ntpPresentationTime); return scheduler; }; // src/WsRtspVideo.tsx var debugLog4 = (0, import_debug4.default)("msp:ws-rtsp-video"); var VideoNative2 = import_styled_components9.default.video` max-height: 100%; object-fit: contain; width: 100%; `; var WsRtspVideo = ({ forwardedRef, play = false, ws, rtsp, autoPlay = true, muted = true, onPlaying, onEnded, onSdp, onRtcp, metadataHandler, offset = 0, autoRetry = false }) => { let videoRef = (0, import_react19.useRef)(null); if (typeof forwardedRef === "function") { forwardedRef(videoRef.current); } else if (forwardedRef) { videoRef = forwardedRef; } const [canplay, unsetCanplay] = useEventState(videoRef, "canplay"); const [playing, unsetPlaying] = useEventState(videoRef, "playing"); const [pipeline, setPipeline] = (0, import_react19.useState)( null ); const [fetching, setFetching] = (0, import_react19.useState)(false); const __offsetRef = (0, import_react19.useRef)(offset); const __rangeRef = (0, import_react19.useRef)([offset, void 0]); const __onPlayingRef = (0, import_react19.useRef)(onPlaying); __onPlayingRef.current = onPlaying; const __onEndedRef = (0, import_react19.useRef)(onEnded); __onEndedRef.current = onEnded; const __sensorTmRef = (0, import_react19.useRef)(); useVideoDebug(videoRef.current, debugLog4); (0, import_react19.useEffect)(() => { var _a; const videoEl = videoRef.current; if (videoEl === null) { return; } if (play && canplay === true && playing === false) { debugLog4("play"); videoEl.play().catch((err) => { console.error("VideoElement error: ", err.message); }); const { videoHeight, videoWidth } = videoEl; debugLog4("%o", { videoHeight, videoWidth }); } else if (!play && playing === true) { debugLog4("pause"); videoEl.pause(); unsetPlaying(); } else if (play && playing === true) { if (__onPlayingRef.current !== void 0) { __onPlayingRef.current({ el: videoEl, pipeline: pipeline != null ? pipeline : void 0, width: videoEl.videoWidth, height: videoEl.videoHeight, formatSupportsAudio: FORMAT_SUPPORTS_AUDIO["RTP_H264" /* RTP_H264 */], volume: ((_a = pipeline == null ? void 0 : pipeline.tracks) == null ? void 0 : _a.find((track) => track.type === "audio")) ? videoEl.volume : void 0, range: __rangeRef.current, sensorTm: __sensorTmRef.current }); } } }, [play, canplay, playing, unsetPlaying, pipeline]); const __metadataHandlerRef = (0, import_react19.useRef)(metadataHandler); __metadataHandlerRef.current = metadataHandler; (0, import_react19.useEffect)(() => { const videoEl = videoRef.current; __offsetRef.current = offset; if (ws !== void 0 && ws.length > 0 && rtsp !== void 0 && rtsp.length > 0 && videoEl !== null) { debugLog4("create pipeline", ws, rtsp); const newPipeline = new import_media_stream_library4.pipelines.Html5VideoPipeline({ ws: { uri: ws }, rtsp: { uri: rtsp }, mediaElement: videoEl }); if (autoRetry) { import_media_stream_library4.utils.addRTSPRetry(newPipeline.rtsp); } setPipeline(newPipeline); let scheduler; if (__metadataHandlerRef.current !== void 0) { scheduler = attachMetadataHandler( newPipeline, __metadataHandlerRef.current ); } return () => { debugLog4("close pipeline and clear video"); newPipeline.close(); videoEl.src = ""; scheduler == null ? void 0 : scheduler.reset(); setPipeline(null); setFetching(false); unsetCanplay(); unsetPlaying(); }; } }, [ws, rtsp, offset, unsetCanplay, unsetPlaying, autoRetry]); const __onSdpRef = (0, import_react19.useRef)(onSdp); __onSdpRef.current = onSdp; const __onRtcpRef = (0, import_react19.useRef)(onRtcp); __onRtcpRef.current = onRtcp; (0, import_react19.useEffect)(() => { if (play && pipeline && !fetching) { pipeline.ready.then(() => { pipeline.onSdp = (sdp) => { var _a; const videoMedia = sdp.media.find((m) => { return m.type === "video"; }); if (videoMedia !== void 0) { __sensorTmRef.current = (_a = videoMedia["x-sensor-transform"]) != null ? _a : videoMedia["transform"]; } if (__onSdpRef.current !== void 0) { __onSdpRef.current(sdp); } }; pipeline.rtsp.onRtcp = (rtcp) => { var _a, _b; (_a = __onRtcpRef.current) == null ? void 0 : _a.call(__onRtcpRef, rtcp); if ((0, import_media_stream_library4.isRtcpBye)(rtcp)) { (_b = __onEndedRef.current) == null ? void 0 : _b.call(__onEndedRef); } }; pipeline.rtsp.onPlay = (range) => { if (range !== void 0) { __rangeRef.current = [ parseFloat(range[0]) || 0, parseFloat(range[1]) || void 0 ]; } }; pipeline.rtsp.play(__offsetRef.current); }).catch((err) => { console.error(err); }); debugLog4("initiated data fetching"); setFetching(true); } }, [play, pipeline, fetching]); return /* @__PURE__ */ import_react19.default.createElement(VideoNative2, { autoPlay, muted, ref: videoRef }); }; // src/PlaybackArea.tsx var debugLog5 = (0, import_debug5.default)("msp:api"); var AxisApi = /* @__PURE__ */ ((AxisApi2) => { AxisApi2["AXIS_IMAGE_CGI"] = "AXIS_IMAGE_CGI"; AxisApi2["AXIS_MEDIA_AMP"] = "AXIS_MEDIA_AMP"; AxisApi2["AXIS_MEDIA_CGI"] = "AXIS_MEDIA_CGI"; AxisApi2["AXIS_MJPEG_CGI"] = "AXIS_MJPEG_CGI"; return AxisApi2; })(AxisApi || {}); var Protocol = /* @__PURE__ */ ((Protocol2) => { Protocol2["HTTP"] = "http:"; Protocol2["HTTPS"] = "https:"; Protocol2["WS"] = "ws:"; Protocol2["WSS"] = "wss:"; return Protocol2; })(Protocol || {}); var FORMAT_API = { RTP_H264: "AXIS_MEDIA_AMP" /* AXIS_MEDIA_AMP */, RTP_JPEG: "AXIS_MEDIA_AMP" /* AXIS_MEDIA_AMP */, MJPEG: "AXIS_MJPEG_CGI" /* AXIS_MJPEG_CGI */, MP4_H264: "AXIS_MEDI