UNPKG

media-stream-player

Version:

Player built on top of media-stream-library

265 lines (264 loc) 11.6 kB
import React, { useState, useRef, useCallback, useEffect } from 'react'; import styled from 'styled-components'; import { DateTime, Duration } from 'luxon'; import { useUserActive } from './hooks/useUserActive'; import { Button } from './components/Button'; import { Play, Pause, Stop, Refresh, CogWheel, Screenshot } from './img'; import { Settings } from './Settings'; function isHTMLMediaElement(el) { return el.buffered !== undefined; } export const ControlArea = styled.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; `; export const ControlBar = styled.div ` width: 100%; height: 32px; background: rgb(0, 0, 0, 0.66); display: flex; align-items: center; padding: 0 16px; box-sizing: border-box; `; const VolumeContainer = styled.div ` margin-left: 8px; `; const Progress = styled.div ` flex-grow: 2; padding: 0 32px; display: flex; align-items: center; `; const ProgressBarContainer = styled.div ` margin: 0; width: 100%; height: 24px; position: relative; display: flex; flex-direction: column; justify-content: center; `; const ProgressBar = styled.div ` background-color: rgba(255, 255, 255, 0.1); height: 1px; position: relative; width: 100%; ${ProgressBarContainer}:hover > & { height: 3px; } `; const ProgressBarPlayed = styled.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%; `; const ProgressBarBuffered = styled.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%; `; const ProgressTimestamp = styled.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; `; const ProgressIndicator = styled.div ` color: rgb(240, 180, 0); padding-left: 24px; font-size: 10px; white-space: nowrap; `; export const Controls = ({ play, videoProperties, duration, startTime, src, parameters, onPlay, onStop, onRefresh, onSeek, onScreenshot, onFormat, onVapix, labels, showStatsOverlay, toggleStats, format, volume, setVolume, }) => { const controlArea = useRef(null); const userActive = useUserActive(controlArea); const [settings, setSettings] = useState(false); const toggleSettings = useCallback(() => setSettings((currentSettings) => !currentSettings), [setSettings]); const onVolumeChange = useCallback((e) => { if (setVolume !== undefined) { setVolume(parseFloat(e.target.value)); } }, [setVolume]); const [totalDuration, setTotalDuration] = useState(duration); const __mediaTimeline = useRef({ startDateTime: startTime !== undefined ? DateTime.fromISO(startTime) : undefined, }); /** * Progress * * Compute progress of played and buffered amounts of media. This includes any * media before the actual start of the video. * * The range on videoProperties specifies where we started to play (meaning, * the time corresponding to currentTime = 0), and where the playback stops. * To avoid having to collect extra data about the actual media length, we * treat the end of the range as the end of the actual media (i.e. a simple * way to establish the duration). * * Example: * - range = [0, undefined] => start from the beginning, unknown end * - range = [8, 19] => start from 8s into the media, stop at 19s in which * case currentTime = 0 is actually 8s. In this case the media is actually * 25s long, but we cannot display that in our progress. So this system * only works correctly when playing back from any starting point till the * end of the media (i.e. no "chunks" within). * * media 0 ------------------------------------------------- 25s * range 8s ----------------------------- 19s * currentTime 0s ----------------------------- 11s * progress 0 ------------------------------------------- 19s * * So we treat the start of the range as offset for total progress, and the * end of the range as total duration. That means we do not handle situations * where the duration is longer than the end of the range. * * When computing progress, if the duration is Infinity (live playback), we * use the total buffered time as a (temporary) duration. */ const [progress, setProgress] = useState({ playedFraction: 0, bufferedFraction: 0, counter: '', }); useEffect(() => { var _a; if (videoProperties === undefined) { return; } const { el, pipeline, range } = videoProperties; if (el === null || pipeline === undefined) { return; } // Extract range and update duration accordingly. const [start = 0, end = duration] = range !== null && range !== void 0 ? range : [0, duration]; const __duration = (_a = duration !== null && duration !== void 0 ? duration : end) !== null && _a !== void 0 ? _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 = `${Duration.fromMillis(played * 1000).toFormat('h:mm:ss')} / ${Duration.fromMillis(total * 1000).toFormat('h:mm:ss')}`; setProgress({ playedFraction: played / total, bufferedFraction: buffered / total, counter, }); }; updateProgress(); // Use progress events on media elements 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); }; } // Use polling when not a media element const progressInterval = setInterval(updateProgress, 1000); return () => { clearInterval(progressInterval); }; }, [videoProperties, duration, startTime, setTotalDuration]); const seek = useCallback((e) => { if (totalDuration === undefined) { return; } const { left, width } = e.currentTarget.getBoundingClientRect(); const fraction = (e.pageX - left) / width; onSeek(fraction * totalDuration); }, [totalDuration, onSeek]); const [timestamp, setTimestamp] = useState({ left: 0, label: '' }); const __progressBarContainerRef = useRef(null); useEffect(() => { if (startTime !== undefined) { __mediaTimeline.current.startDateTime = DateTime.fromISO(startTime); } const el = __progressBarContainerRef.current; if (el === null || totalDuration === undefined) { return; } const { left, width } = el.getBoundingClientRect(); const showTimestamp = (e) => { const offset = e.pageX - left; const offsetMillis = (offset / width) * totalDuration * 1000; setTimestamp({ left: offset, label: __mediaTimeline.current.startDateTime !== undefined ? __mediaTimeline.current.startDateTime .plus(offsetMillis) .toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS) : 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 (React.createElement(ControlArea, { ref: controlArea, visible: play !== true || settings || userActive }, React.createElement(ControlBar, null, React.createElement(Button, { onClick: onPlay }, play === true ? (React.createElement(Pause, { title: labels === null || labels === void 0 ? void 0 : labels.pause })) : (React.createElement(Play, { title: labels === null || labels === void 0 ? void 0 : labels.play }))), src !== undefined && (React.createElement(Button, { onClick: onStop }, React.createElement(Stop, { title: labels === null || labels === void 0 ? void 0 : labels.stop }))), src !== undefined && (React.createElement(Button, { onClick: onRefresh }, React.createElement(Refresh, { title: labels === null || labels === void 0 ? void 0 : labels.refresh }))), src !== undefined && (React.createElement(Button, { onClick: onScreenshot }, React.createElement(Screenshot, { title: labels === null || labels === void 0 ? void 0 : labels.screenshot }))), volume !== undefined ? (React.createElement(VolumeContainer, { title: labels === null || labels === void 0 ? void 0 : labels.volume }, React.createElement("input", { type: "range", min: "0", max: "1", step: "0.05", onChange: onVolumeChange, value: volume !== null && volume !== void 0 ? volume : 0 }))) : null, React.createElement(Progress, null, React.createElement(ProgressBarContainer, { onClick: seek, ref: __progressBarContainerRef }, React.createElement(ProgressBar, null, React.createElement(ProgressBarPlayed, { fraction: progress.playedFraction }), React.createElement(ProgressBarBuffered, { fraction: progress.bufferedFraction }), timestamp.left !== 0 ? (React.createElement(ProgressTimestamp, { left: timestamp.left }, timestamp.label)) : null)), React.createElement(ProgressIndicator, null, totalDuration === Infinity ? '∙ LIVE' : progress.counter)), React.createElement(Button, { onClick: toggleSettings }, React.createElement(CogWheel, { title: labels === null || labels === void 0 ? void 0 : labels.settings }))), settings && (React.createElement(Settings, { parameters: parameters, format: format, onFormat: onFormat, onVapix: onVapix, showStatsOverlay: showStatsOverlay, toggleStats: toggleStats })))); }; //# sourceMappingURL=Controls.js.map