media-stream-player
Version:
Player built on top of media-stream-library
265 lines (264 loc) • 11.6 kB
JavaScript
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