diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js
index 2ffc02c7d..5e4a9f3ee 100644
--- a/src/routes/Player/ControlBar/ControlBar.js
+++ b/src/routes/Player/ControlBar/ControlBar.js
@@ -24,6 +24,8 @@ const ControlBar = ({
audioTracks,
metaItem,
nextVideo,
+ stream,
+ statistics,
onPlayRequested,
onPauseRequested,
onMuteRequested,
@@ -35,6 +37,7 @@ const ControlBar = ({
onToggleSpeedMenu,
onToggleVideosMenu,
onToggleOptionsMenu,
+ onToggleStatisticsMenu,
...props
}) => {
const { chromecast } = useServices();
@@ -55,6 +58,9 @@ const ControlBar = ({
const onOptionsButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.optionsMenuClosePrevented = true;
}, []);
+ const onStatisticsButtonMouseDown = React.useCallback((event) => {
+ event.nativeEvent.statisticsMenuClosePrevented = true;
+ }, []);
const onPlayPauseButtonClick = React.useCallback(() => {
if (paused) {
if (typeof onPlayRequested === 'function') {
@@ -111,6 +117,11 @@ const ControlBar = ({
onToggleOptionsMenu();
}
}, [onToggleOptionsMenu]);
+ const onStatisticsButtonClick = React.useCallback(() => {
+ if (typeof onToggleStatisticsMenu === 'function') {
+ onToggleStatisticsMenu();
+ }
+ }, [onToggleStatisticsMenu]);
const onChromecastButtonClick = React.useCallback(() => {
chromecast.transport.requestSession();
}, []);
@@ -165,12 +176,12 @@ const ControlBar = ({
+
-
@@ -209,6 +220,8 @@ ControlBar.propTypes = {
audioTracks: PropTypes.array,
metaItem: PropTypes.object,
nextVideo: PropTypes.object,
+ stream: PropTypes.object,
+ statistics: PropTypes.object,
onPlayRequested: PropTypes.func,
onPauseRequested: PropTypes.func,
onMuteRequested: PropTypes.func,
@@ -220,6 +233,7 @@ ControlBar.propTypes = {
onToggleSpeedMenu: PropTypes.func,
onToggleVideosMenu: PropTypes.func,
onToggleOptionsMenu: PropTypes.func,
+ onToggleStatisticsMenu: PropTypes.func,
};
module.exports = ControlBar;
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index a37ceb588..0e2b43535 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -13,6 +13,7 @@ const Icon = require('@stremio/stremio-icons/dom');
const BufferingLoader = require('./BufferingLoader');
const ControlBar = require('./ControlBar');
const NextVideoPopup = require('./NextVideoPopup');
+const StatisticsMenu = require('./StatisticsMenu');
const InfoMenu = require('./InfoMenu');
const OptionsMenu = require('./OptionsMenu');
const VideosMenu = require('./VideosMenu');
@@ -81,6 +82,7 @@ const Player = ({ urlParams, queryParams }) => {
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
+ const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
const nextVideoPopupDismissed = React.useRef(false);
const defaultSubtitlesSelected = React.useRef(false);
const defaultAudioTrackSelected = React.useRef(false);
@@ -234,6 +236,9 @@ const Player = ({ urlParams, queryParams }) => {
if (!event.nativeEvent.videosMenuClosePrevented) {
closeVideosMenu();
}
+ if (!event.nativeEvent.statisticsMenuClosePrevented) {
+ closeStatisticsMenu();
+ }
}, []);
const onContainerMouseMove = React.useCallback((event) => {
setImmersed(false);
@@ -356,6 +361,26 @@ const Player = ({ urlParams, queryParams }) => {
}
}
}, [player.nextVideo, videoState.time, videoState.duration]);
+ React.useEffect(() => {
+ if (player.selected && player.selected.stream && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
+ const { infoHash, fileIdx } = player.selected.stream;
+ const getStatistics = () => {
+ core.transport.dispatch({
+ action: 'StreamingServer',
+ args: {
+ action: 'GetStatistics',
+ args: {
+ infoHash,
+ fileIdx,
+ }
+ }
+ });
+ };
+ getStatistics();
+ const statisticsInterval = setInterval(getStatistics, 5000);
+ return () => clearInterval(statisticsInterval);
+ }
+ }, [player.selected]);
React.useEffect(() => {
if (!defaultSubtitlesSelected.current) {
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
@@ -445,7 +470,7 @@ const Player = ({ urlParams, queryParams }) => {
const onKeyDown = (event) => {
switch (event.code) {
case 'Space': {
- if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.paused !== null) {
+ if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.paused !== null) {
if (videoState.paused) {
onPlayRequested();
} else {
@@ -456,7 +481,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowRight': {
- if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.time !== null) {
+ if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.time !== null) {
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier));
}
@@ -464,7 +489,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowLeft': {
- if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.time !== null) {
+ if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.time !== null) {
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier));
}
@@ -472,14 +497,14 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowUp': {
- if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.volume !== null) {
+ if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume + 5);
}
break;
}
case 'ArrowDown': {
- if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && videoState.volume !== null) {
+ if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume - 5);
}
@@ -490,6 +515,7 @@ const Player = ({ urlParams, queryParams }) => {
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
+ closeStatisticsMenu();
if ((Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) ||
(Array.isArray(videoState.extraSubtitlesTracks) && videoState.extraSubtitlesTracks.length > 0) ||
(Array.isArray(videoState.audioTracks) && videoState.audioTracks.length > 0)) {
@@ -503,6 +529,7 @@ const Player = ({ urlParams, queryParams }) => {
closeSubtitlesMenu();
closeSpeedMenu();
closeVideosMenu();
+ closeStatisticsMenu();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
toggleInfoMenu();
}
@@ -514,6 +541,7 @@ const Player = ({ urlParams, queryParams }) => {
closeInfoMenu();
closeSubtitlesMenu();
closeVideosMenu();
+ closeStatisticsMenu();
if (videoState.playbackSpeed !== null) {
toggleSpeedMenu();
}
@@ -525,18 +553,32 @@ const Player = ({ urlParams, queryParams }) => {
closeInfoMenu();
closeSubtitlesMenu();
closeSpeedMenu();
+ closeStatisticsMenu();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
toggleVideosMenu();
}
break;
}
+ case 'KeyD': {
+ closeOptionsMenu();
+ closeInfoMenu();
+ closeSubtitlesMenu();
+ closeSpeedMenu();
+ closeVideosMenu();
+ if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
+ toggleStatisticsMenu();
+ }
+
+ break;
+ }
case 'Escape': {
closeOptionsMenu();
closeSubtitlesMenu();
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
+ closeStatisticsMenu();
onDismissNextVideoPopup();
break;
}
@@ -548,7 +590,7 @@ const Player = ({ urlParams, queryParams }) => {
return () => {
window.removeEventListener('keydown', onKeyDown);
};
- }, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, optionsMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
+ }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, optionsMenuOpen, statisticsMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]);
React.useLayoutEffect(() => {
return () => {
setImmersedDebounced.cancel();
@@ -557,7 +599,7 @@ const Player = ({ urlParams, queryParams }) => {
};
}, []);
return (
-
{
null
}
{
- subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen || optionsMenuOpen ?
+ subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen || optionsMenuOpen || statisticsMenuOpen ?
:
null
@@ -633,6 +675,8 @@ const Player = ({ urlParams, queryParams }) => {
audioTracks={videoState.audioTracks}
metaItem={player.metaItem}
nextVideo={player.nextVideo}
+ stream={player.selected !== null ? player.selected.stream : null}
+ statistics={streamingServer.statistics}
onPlayRequested={onPlayRequested}
onPauseRequested={onPauseRequested}
onMuteRequested={onMuteRequested}
@@ -644,6 +688,7 @@ const Player = ({ urlParams, queryParams }) => {
onToggleInfoMenu={toggleInfoMenu}
onToggleSpeedMenu={toggleSpeedMenu}
onToggleVideosMenu={toggleVideosMenu}
+ onToggleStatisticsMenu={toggleStatisticsMenu}
onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove}
/>
@@ -659,6 +704,16 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
+ {
+ statisticsMenuOpen ?
+
+ :
+ null
+ }
{
subtitlesMenuOpen ?
{
+ const peers = React.useMemo(() => {
+ return statistics.type === 'Ready' && statistics.content?.peers ?
+ statistics.content.peers
+ :
+ 0;
+ }, [statistics]);
+
+ const speed = React.useMemo(() => {
+ return statistics.type === 'Ready' && statistics.content?.downloadSpeed ?
+ (statistics.content.downloadSpeed / 1000 / 1000).toFixed(2)
+ :
+ 0;
+ }, [statistics]);
+
+ const completed = React.useMemo(() => {
+ return statistics.type === 'Ready' && statistics.content?.streamProgress ?
+ (statistics.content.streamProgress * 100).toFixed(2)
+ :
+ 0;
+ }, [statistics]);
+
+ return (
+
+
+ Statistics
+
+
+
+
+ Peers
+
+
+ { peers }
+
+
+
+
+ Speed
+
+
+ { speed } MB/s
+
+
+
+
+ Completed
+
+
+ { completed } %
+
+
+
+
+
+ Info Hash
+
+
+ { stream.infoHash }
+
+
+
+ );
+};
+
+StatisticsMenu.propTypes = {
+ className: PropTypes.string,
+ stream: PropTypes.object,
+ statistics: PropTypes.object,
+};
+
+module.exports = StatisticsMenu;
diff --git a/src/routes/Player/StatisticsMenu/index.js b/src/routes/Player/StatisticsMenu/index.js
new file mode 100644
index 000000000..fc0c982a6
--- /dev/null
+++ b/src/routes/Player/StatisticsMenu/index.js
@@ -0,0 +1,4 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+const StatisticsMenu = require('./StatisticsMenu');
+module.exports = StatisticsMenu;
diff --git a/src/routes/Player/StatisticsMenu/styles.less b/src/routes/Player/StatisticsMenu/styles.less
new file mode 100644
index 000000000..d25f14c41
--- /dev/null
+++ b/src/routes/Player/StatisticsMenu/styles.less
@@ -0,0 +1,52 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
+
+.statistics-menu-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ width: 30rem;
+ padding: 1.5rem;
+
+ .title {
+ flex: none;
+ font-weight: 600;
+ color: @color-surface-light5-90;
+ }
+
+ .label {
+ flex: none;
+ font-weight: 500;
+ color: @color-surface-light5-50;
+ }
+
+ .value {
+ flex: none;
+ font-weight: 500;
+ color: @color-surface-light5-90;
+ }
+
+ .stats {
+ flex: auto;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: 1rem;
+
+ .stat {
+ flex: auto;
+ display: flex;
+ flex-direction: row;
+ gap: 0.5rem;
+ }
+ }
+
+ .info-hash {
+ flex: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+}
\ No newline at end of file
diff --git a/src/types/models/StremingServer.d.ts b/src/types/models/StremingServer.d.ts
index 2a0495cc3..4f12f4a76 100644
--- a/src/types/models/StremingServer.d.ts
+++ b/src/types/models/StremingServer.d.ts
@@ -25,11 +25,86 @@ type StreamingServerSettings = {
serverVersion: string,
};
+type SFile = {
+ name: string,
+ path: string,
+ length: number,
+ offset: number,
+};
+
+type Source = {
+ last_started: string,
+ numFound: number,
+ numFoundUniq: number,
+ numRequests: number,
+ url: string,
+}
+
+type Growler = {
+ flood: number,
+ pulse: number,
+}
+
+type PeerSearch = {
+ max: number,
+ min: number,
+ sources: string[],
+}
+
+type SwarmCap = {
+ maxSpeed: number,
+ minPeers: number,
+}
+
+type Options = {
+ connections: number,
+ dht: boolean,
+ growler: Growler,
+ handshakeTimeout: number,
+ path: string,
+ peerSearch: PeerSearch,
+ swarmCap: SwarmCap,
+ timeout: number,
+ tracker: boolean,
+ virtual: boolean,
+}
+
+type Statistics = {
+ name: string,
+ infoHash: string,
+ files: SFile[],
+ sources: Source[],
+ opts: Options,
+ downloadSpeed: number,
+ uploadSpeed: number,
+ downloaded: number,
+ uploaded: number,
+ unchoked: number,
+ peers: number,
+ queued: number,
+ unique: number,
+ connectionTries: number,
+ peerSearchRunning: boolean,
+ streamLen: number,
+ streamName: string,
+ streamProgress: number,
+ swarmConnections: number,
+ swarmPaused: boolean,
+ swarmSize: number,
+};
+
+type Selected = {
+ transportUrl: string,
+ statistics: {
+ infoHash: string,
+ fileIdx: number,
+ } | null
+};
+
type StreamingServer = {
baseUrl: Loadable | null,
- selected: {
- transportUrl: string,
- } | null,
+ selected: Selected | null,
settings: Loadable | null,
torrent: [string, Loadable] | null,
+ statistics: Loadable | null,
};
\ No newline at end of file