Merge branch 'development' into fix/streaming-server-state

This commit is contained in:
Lachezar Lechev 2025-12-22 14:55:20 +02:00
commit d6e62a5eec
No known key found for this signature in database
GPG key ID: 69BDCB3ED8CE8037
20 changed files with 126 additions and 77 deletions

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: gh-pages
fetch-depth: 0

View file

@ -9,8 +9,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install NPM dependencies
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Install dependencies
run: pnpm install
- name: Build
env:
@ -19,7 +24,7 @@ jobs:
- name: Zip build artifact
run: zip -r stremio-web.zip ./build
- name: Upload build artifact to GitHub release assets
uses: svenstaro/upload-release-action@2.11.2
uses: svenstaro/upload-release-action@2.11.3
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: stremio-web.zip

View file

@ -1,7 +1,7 @@
{
"name": "stremio",
"displayName": "Stremio",
"version": "5.0.0-beta.27",
"version": "5.0.0-beta.29",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
@ -17,8 +17,8 @@
"@babel/runtime": "7.26.0",
"@sentry/browser": "8.42.0",
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.50.0",
"@stremio/stremio-icons": "5.7.1",
"@stremio/stremio-core-web": "0.51.1",
"@stremio/stremio-icons": "5.8.0",
"@stremio/stremio-video": "0.0.64",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",

View file

@ -18,11 +18,11 @@ importers:
specifier: 5.2.0
version: 5.2.0
'@stremio/stremio-core-web':
specifier: 0.50.0
version: 0.50.0
specifier: 0.51.1
version: 0.51.1
'@stremio/stremio-icons':
specifier: 5.7.1
version: 5.7.1
specifier: 5.8.0
version: 5.8.0
'@stremio/stremio-video':
specifier: 0.0.64
version: 0.0.64
@ -1302,11 +1302,11 @@ packages:
'@stremio/stremio-colors@5.2.0':
resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==}
'@stremio/stremio-core-web@0.50.0':
resolution: {integrity: sha512-SRE9nStgYNbhjJAw7mXfmM0wdnSLS4GMSJsSMTXvoGxnUgd+yisJUkN/9Sughe4t2IU7Uct8QWpdx9zFdlil+g==}
'@stremio/stremio-core-web@0.51.1':
resolution: {integrity: sha512-BD8i6zkDdMPeCyH50Bb7SB8r4nYx4eJwz4kLEJEl0PFjdr0gOmwHtEIgNa89ShJLNXUjPnpv4sVSNxFRG8fb5Q==}
'@stremio/stremio-icons@5.7.1':
resolution: {integrity: sha512-Z96p36LLX3G+ewMnFKmNZVsO/AtcHA33WQ3wGOYFubxiYADPRAkcLVU5rHIfiGSC9IUaUVhxQWTPVB9ScY4Q5Q==}
'@stremio/stremio-icons@5.8.0':
resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==}
'@stremio/stremio-video@0.0.64':
resolution: {integrity: sha512-29w/lwU8BB6ai8LUyCnpRc2F9kPf7cpys40NCobt70MqBP/UqvYISsrnD/ijoBwvtpKdZ6ptv5h9BbDj6rrerw==}
@ -6561,11 +6561,11 @@ snapshots:
'@stremio/stremio-colors@5.2.0': {}
'@stremio/stremio-core-web@0.50.0':
'@stremio/stremio-core-web@0.51.1':
dependencies:
'@babel/runtime': 7.24.1
'@stremio/stremio-icons@5.7.1': {}
'@stremio/stremio-icons@5.8.0': {}
'@stremio/stremio-video@0.0.64':
dependencies:

View file

@ -48,7 +48,7 @@
--color-x: #000000;
--color-reddit: #FF4500;
--color-imdb: #f5c518;
--color-trakt: #ED2224;
--color-trakt: rgb(255, 255, 255);
--color-placeholder: #60606080;
--color-placeholder-text: @color-surface-50;
--color-placeholder-background: @color-surface-dark5-20;

View file

@ -3,18 +3,18 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { Button } = require('stremio/components');
const useTranslate = require('stremio/common/useTranslate');
const styles = require('./styles');
const MetaLinks = ({ className, label, links }) => {
const { t } = useTranslation();
const { string, stringWithPrefix } = useTranslate();
return (
<div className={classnames(className, styles['meta-links-container'])}>
{
typeof label === 'string' && label.length > 0 ?
<div className={styles['label-container']}>
{t(`LINKS_${label.toUpperCase()}`)}
{ stringWithPrefix(label.toUpperCase(), 'LINKS') }
</div>
:
null
@ -24,7 +24,7 @@ const MetaLinks = ({ className, label, links }) => {
<div className={styles['links-container']}>
{links.map(({ label, href }, index) => (
<Button key={index} className={styles['link-container']} title={label} href={href}>
{ t(label) }
{ string(label) }
</Button>
))}
</div>

View file

@ -1,7 +1,7 @@
.shortcuts-group {
flex: 1 1 0;
position: relative;
min-width: 30rem;
width: 30rem;
display: flex;
flex-direction: column;
gap: 2rem;

View file

@ -142,11 +142,11 @@ const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabl
<div className={styles['layer']}>
<div
className={classnames(styles['track-after'], { [styles['audio-boost']]: audioBoost })}
style={{ '--mask-width': `calc(${thumbPosition} * 100%)` }}
style={{ '--mask-width': `calc(${thumbPosition.toFixed(3)} * 100%)` }}
/>
</div>
<div className={styles['layer']}>
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} />
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition.toFixed(3)})` }} />
</div>
</div>
);

View file

@ -73,15 +73,19 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
const blurThumbnail = profile.settings.hideSpoilers && season && episode && !watched;
React.useEffect(() => {
selected && !watched && ref.current?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'start'
});
if (selected && ref.current) {
if ((progress && watched) || !watched) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'start'
});
}
}
}, [selected]);
return (
<Button {...props} ref={ref} className={classnames(className, styles['video-container'])} title={title}>
<Button {...props} ref={ref} className={classnames(className, styles['video-container'], { [styles['selected']]: selected })} title={title}>
{
typeof thumbnail === 'string' && thumbnail.length > 0 ?
<div className={styles['thumbnail-container']}>

View file

@ -19,6 +19,11 @@
padding: 0.5rem;
margin-bottom: 0.5rem;
border-radius: var(--border-radius);
border: 0.15rem solid transparent;
@supports (scroll-margin: 1.25rem) {
scroll-margin: 1.25rem;
}
&:hover,
&:focus,
@ -172,6 +177,20 @@
}
}
&.selected {
animation: border 3s ease-in-out forwards;
}
@keyframes border {
0% {
border: 0.15rem solid var(--primary-accent-color);
}
100% {
border: 0.15rem solid transparent;
}
}
.context-menu-container {
max-width: calc(90% - 1.5rem);
z-index: 2;

View file

@ -50,7 +50,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
remoteAddons.selected !== null ?
t.stringWithPrefix(remoteAddons.selected.request.path.type, 'TYPE_')
:
typeSelect.title;
t.string('SELECT_TYPE');
},
onSelect: (value) => {
window.location = value;

View file

@ -22,12 +22,11 @@ const Board = () => {
const profile = useProfile();
const boardCatalogsOffset = continueWatchingPreview.items.length > 0 ? 1 : 0;
const scrollContainerRef = React.useRef();
const streamingServerWarningDismissed = React.useMemo(() => {
return streamingServer.settings !== null && (streamingServer.state.type === 'Ready' && streamingServer.state.content === 'running') || (
!isNaN(profile.settings.streamingServerWarningDismissed.getTime()) &&
profile.settings.streamingServerWarningDismissed.getTime() > Date.now()
);
}, [profile.settings, streamingServer.state]);
const showStreamingServerWarning = React.useMemo(() => {
return streamingServer.settings !== null && (streamingServer.state.type !== 'Ready' || streamingServer.state.content === 'running') || (
isNaN(profile.settings.streamingServerWarningDismissed.getTime()) ||
profile.settings.streamingServerWarningDismissed.getTime() < Date.now());
}, [profile.settings, streamingServer.settings]);
const onVisibleRangeChange = React.useCallback(() => {
const range = getVisibleChildrenRange(scrollContainerRef.current);
if (range === null) {
@ -103,7 +102,7 @@ const Board = () => {
</div>
</MainNavBars>
{
!streamingServerWarningDismissed ?
showStreamingServerWarning ?
<StreamingServerWarning className={styles['board-warning-container']} />
:
null

View file

@ -54,8 +54,8 @@ const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks,
}
}, [streamingUrl, downloadUrl]);
const onDownloadVideoButtonClick = React.useCallback(() => {
if (streamingUrl || downloadUrl) {
platform.openExternal(streamingUrl || downloadUrl);
if (downloadUrl || streamingUrl ) {
platform.openExternal(downloadUrl || streamingUrl);
}
}, [streamingUrl, downloadUrl]);

View file

@ -8,7 +8,7 @@ const langs = require('langs');
const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { onFileDrop, useSettings, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform } = require('stremio/common');
const { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform } = require('stremio/common');
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
@ -36,7 +36,7 @@ const Player = ({ urlParams, queryParams }) => {
const forceTranscoding = React.useMemo(() => {
return queryParams.has('forceTranscoding');
}, [queryParams]);
const profile = useProfile();
const [player, videoParamsChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings();
const streamingServer = useStreamingServer();
@ -105,13 +105,27 @@ const Player = ({ urlParams, queryParams }) => {
video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
}, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]);
const handleNextVideoNavigation = React.useCallback((deepLinks) => {
if (deepLinks.player) {
isNavigating.current = true;
window.location.replace(deepLinks.player);
} else if (deepLinks.metaDetailsStreams) {
isNavigating.current = true;
window.location.replace(deepLinks.metaDetailsStreams);
const handleNextVideoNavigation = React.useCallback((deepLinks, bingeWatching, ended) => {
if (ended) {
if (bingeWatching) {
if (deepLinks.player) {
isNavigating.current = true;
window.location.replace(deepLinks.player);
} else if (deepLinks.metaDetailsStreams) {
isNavigating.current = true;
window.location.replace(deepLinks.metaDetailsStreams);
}
} else {
window.history.back();
}
} else {
if (deepLinks.player) {
isNavigating.current = true;
window.location.replace(deepLinks.player);
} else if (deepLinks.metaDetailsStreams) {
isNavigating.current = true;
window.location.replace(deepLinks.metaDetailsStreams);
}
}
}, []);
@ -127,7 +141,8 @@ const Player = ({ urlParams, queryParams }) => {
nextVideo();
const deepLinks = window.playerNextVideo.deepLinks;
handleNextVideoNavigation(deepLinks);
handleNextVideoNavigation(deepLinks, profile.settings.bingeWatching, true);
} else {
window.history.back();
}
@ -257,9 +272,9 @@ const Player = ({ urlParams, queryParams }) => {
nextVideo();
const deepLinks = player.nextVideo.deepLinks;
handleNextVideoNavigation(deepLinks);
handleNextVideoNavigation(deepLinks, profile.settings.bingeWatching, false);
}
}, [player.nextVideo, handleNextVideoNavigation]);
}, [player.nextVideo, handleNextVideoNavigation, profile.settings]);
const onVideoClick = React.useCallback(() => {
if (video.state.paused !== null) {
@ -323,10 +338,10 @@ const Player = ({ urlParams, queryParams }) => {
setError(null);
video.unload();
if (player.selected && streamingServer.settings?.type !== 'Loading') {
if (player.selected && player.stream?.type === 'Ready' && streamingServer.settings?.type !== 'Loading') {
video.load({
stream: {
...player.selected.stream,
...player.stream.content,
subtitles: Array.isArray(player.selected.stream.subtitles) ?
player.selected.stream.subtitles.map((subtitles) => ({
...subtitles,
@ -361,7 +376,7 @@ const Player = ({ urlParams, queryParams }) => {
shellTransport: services.shell.active ? services.shell.transport : null,
});
}
}, [streamingServer.baseUrl, player.selected, forceTranscoding, casting]);
}, [streamingServer.baseUrl, player.selected, player.stream, forceTranscoding, casting]);
React.useEffect(() => {
if (video.state.stream !== null) {
const tracks = player.subtitles.map((subtitles) => ({
@ -412,7 +427,7 @@ const Player = ({ urlParams, queryParams }) => {
}, [video.state.videoParams]);
React.useEffect(() => {
if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) {
if (player.nextVideo !== null && !nextVideoPopupDismissed.current) {
if (video.state.time !== null && video.state.duration !== null && video.state.time < video.state.duration && (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration) {
openNextVideoPopup();
} else {

View file

@ -7,11 +7,12 @@ const useStatistics = (player, streamingServer) => {
const { core } = useServices();
const stream = React.useMemo(() => {
return player.selected?.stream ?
player.selected.stream
:
null;
}, [player.selected]);
if (player.stream?.type === 'Ready') {
return player.stream.content;
} else {
return null;
}
}, [player.stream]);
const infoHash = React.useMemo(() => {
return stream?.infoHash ?

View file

@ -152,7 +152,15 @@ const useVideo = () => {
video.current.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.current.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
return () => video.current.destroy();
return () => {
if (video.current) {
try {
video.current.destroy();
} catch (err) {
console.error('Error destroying video:', err);
}
}
};
}, []);
return {

View file

@ -111,7 +111,6 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
<Option label={'SETTINGS_NEXT_VIDEO_POPUP_DURATION'}>
<MultiselectMenu
className={'multiselect'}
disabled={!profile.settings.bingeWatching}
{...nextVideoPopupDurationSelect}
/>
</Option>

View file

@ -4,7 +4,7 @@ import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from '
import classnames from 'classnames';
import throttle from 'lodash.throttle';
import { useRouteFocused } from 'stremio-router';
import { useProfile, useStreamingServer, withCoreSuspender } from 'stremio/common';
import { usePlatform, useProfile, useStreamingServer, withCoreSuspender } from 'stremio/common';
import { MainNavBars } from 'stremio/components';
import { SECTIONS } from './constants';
import Menu from './Menu';
@ -18,6 +18,7 @@ import styles from './Settings.less';
const Settings = () => {
const { routeFocused } = useRouteFocused();
const profile = useProfile();
const platform = usePlatform();
const streamingServer = useStreamingServer();
const sectionsContainerRef = useRef<HTMLDivElement>(null);
@ -37,14 +38,10 @@ const Settings = () => {
const updateSelectedSectionId = useCallback(() => {
const container = sectionsContainerRef.current;
if (container!.scrollTop + container!.clientHeight >= container!.scrollHeight - 50) {
setSelectedSectionId(sections[sections.length - 1].id);
} else {
for (let i = sections.length - 1; i >= 0; i--) {
if (sections[i].ref.current!.offsetTop - container!.offsetTop <= container!.scrollTop) {
setSelectedSectionId(sections[i].id);
break;
}
for (const section of sections) {
const sectionContainer = section.ref.current;
if (sectionContainer && (sectionContainer.offsetTop + container!.offsetTop) < container!.scrollTop + 50) {
setSelectedSectionId(section.id);
}
}
}, []);
@ -94,7 +91,9 @@ const Settings = () => {
profile={profile}
streamingServer={streamingServer}
/>
<Shortcuts ref={shortcutsSectionRef} />
{
!platform.isMobile && <Shortcuts ref={shortcutsSectionRef} />
}
<Info streamingServer={streamingServer} />
</div>
</div>

View file

@ -22,8 +22,8 @@
gap: 0.75rem;
.icon {
width: 3rem;
height: 3rem;
width: 4rem;
height: 4rem;
color: var(--primary-foreground-color);
}