diff --git a/thrimbletrimmer/package.json b/thrimbletrimmer/package.json index 2a9a0f2..264a3e5 100644 --- a/thrimbletrimmer/package.json +++ b/thrimbletrimmer/package.json @@ -22,6 +22,7 @@ "dependencies": { "hls.js": "1.5.17", "luxon": "3.4.4", + "media-icons": "1.1.5", "solid-js": "1.9.3", "vidstack": "1.12.12" } diff --git a/thrimbletrimmer/src/common/video.module.scss b/thrimbletrimmer/src/common/video.module.scss index f3f27b1..f40dd5c 100644 --- a/thrimbletrimmer/src/common/video.module.scss +++ b/thrimbletrimmer/src/common/video.module.scss @@ -8,51 +8,4 @@ .streamTimeSettingLabel { margin-right: 3px; -} - -.videoControls { - font-size: 110%; -} - -.videoControls select { - appearance: none; - font-size: inherit; - background: inherit; - border: none; - text-align: center; -} - -.videoControls option { - background: #222; - padding: 2px; -} - -.videoControlsBar { - display: flex; - align-items: center; - gap: 8px; -} - -.videoControlsSpacer { - flex-grow: 1; -} - -.videoControlsVolume { - display: flex; - align-items: center; - gap: 2px; -} - -.videoControlsVolumeLevel { - width: 100px; - height: 8px; - border-radius: 0; - border: 0; -} - -.videoControlsPlaybackPosition { - height: 10px; - border-radius: 0; - border: 0; - width: 100%; } \ No newline at end of file diff --git a/thrimbletrimmer/src/common/video.scss b/thrimbletrimmer/src/common/video.scss new file mode 100644 index 0000000..953d1e4 --- /dev/null +++ b/thrimbletrimmer/src/common/video.scss @@ -0,0 +1,29 @@ +// Used to make the controls appear below the video player +media-player { + flex-direction: column; +} + +media-provider, media-captions, video { + max-width: 100%; + max-height: 50vh; +} + +// Used to make the controls appear below the video player +media-player:not([data-fullscreen]) media-controls { + position: relative; + height: auto; +} + +media-controls-group { + display: flex; + align-items: center; + width: 100%; +} + +media-volume-slider { + flex-basis: 100px; +} + +.vds-slider-track-fill { + background-color: #f6f6f6; +} \ No newline at end of file diff --git a/thrimbletrimmer/src/common/video.tsx b/thrimbletrimmer/src/common/video.tsx index 1a7bbb2..7174fa3 100644 --- a/thrimbletrimmer/src/common/video.tsx +++ b/thrimbletrimmer/src/common/video.tsx @@ -5,6 +5,7 @@ import { createSignal, For, onCleanup, + onMount, Setter, Show, } from "solid-js"; @@ -19,14 +20,15 @@ import { dateTimeFromTimeAgo, } from "./convertTime"; import styles from "./video.module.scss"; +import "./video.scss"; import { MediaPlayerElement } from "vidstack/elements"; -import { VideoQuality } from "vidstack"; -import playImage from "../images/video-controls/play.png"; -import pauseImage from "../images/video-controls/pause.png"; -import volumeImage from "../images/video-controls/volume.png"; -import volumeMuteImage from "../images/video-controls/volume-mute.png"; -import fullscreenImage from "../images/video-controls/fullscreen.png"; +import "vidstack/icons"; +import "vidstack/player/styles/default/theme.css"; +import "vidstack/player/styles/default/layouts/video.css"; +import "vidstack/player"; +import "vidstack/player/layouts/default"; +import "vidstack/player/ui"; export const VIDEO_FRAMES_PER_SECOND = 30; @@ -196,158 +198,155 @@ export const StreamTimeSettings: Component = (props) => ); }; -export interface VideoControlsProps { - mediaPlayer: Accessor; +export interface VideoPlayerProps { + src: Accessor; } -export const VideoControls: Component = (props) => { - const mediaPlayer = props.mediaPlayer(); - if (!mediaPlayer) { - return <>; - } - - const [isPlaying, setIsPlaying] = createSignal(!props.mediaPlayer().paused); - const [playerTime, setPlayerTime] = createSignal(props.mediaPlayer().currentTime); - const [duration, setDuration] = createSignal(props.mediaPlayer().duration); - const [isMuted, setIsMuted] = createSignal(props.mediaPlayer().muted); - const [volume, setVolume] = createSignal(props.mediaPlayer().volume); - const [playbackRate, setPlaybackRate] = createSignal(props.mediaPlayer().playbackRate); - const [qualityLevel, setQualityLevel] = createSignal( - props.mediaPlayer().state.quality, - ); - const [qualityLevelList, setQualityLevelList] = createSignal(props.mediaPlayer().state.qualities); - const [isFullscreen, setIsFullscreen] = createSignal(false); - - const unsubscribe = props.mediaPlayer().subscribe((playerState) => { - setIsPlaying(!playerState.paused); - setPlayerTime(playerState.currentTime); - setDuration(playerState.duration); - setIsMuted(playerState.muted); - setVolume(playerState.volume); - setPlaybackRate(playerState.playbackRate); - setQualityLevel(playerState.quality); - setQualityLevelList(playerState.qualities); - setIsFullscreen(playerState.fullscreen); - - if (playerState.fullscreen) { - props.mediaPlayer().controls.show(); - } else { - props.mediaPlayer().controls.hide(); - } - }); - +export const VideoPlayer: Component = (props) => { + let [mediaPlayer, setMediaPlayer] = createSignal(); createEffect(() => { - const player = props.mediaPlayer(); - if (isFullscreen() && !player.controls.showing) { - player.controls.show(); - } else if (!isFullscreen() && player.controls.showing) { - player.controls.hide(); - } + const player = mediaPlayer(); + const srcURL = props.src(); + player.src = srcURL; }); - onCleanup(() => unsubscribe()); + let [playerTime, setPlayerTime] = createSignal(0); + let [duration, setDuration] = createSignal(0); + + onMount(() => { + const player = mediaPlayer(); + player.subscribe(({ currentTime, duration }) => { + setPlayerTime(currentTime); + setDuration(duration); + }); + player.streamType = "on-demand"; + }); - const timeDisplay = (time: number) => { + // The elements provided by vidstack don't show milliseconds, so + // we need to run our own for millisecond display. + const formatTime = (time: number) => { const hours = Math.floor(time / 3600); const minutes = Math.floor((time / 60) % 60); - const seconds = Math.floor((time % 60) * 1000) / 1000; + const milliseconds = Math.floor((time % 1) * 1000); + const seconds = Math.floor(time % 60); - const minutesDisplay = minutes < 10 ? `0${minutes}` : minutes.toString(); - const secondsDisplay = seconds < 10 ? `0${seconds}` : seconds.toString(); + const minutesDisplay = minutes.toString().padStart(2, "0"); + const secondsDisplay = seconds.toString().padStart(2, "0"); + const millisecondsDisplay = milliseconds.toString().padStart(3, "0"); if (hours === 0) { - return `${minutesDisplay}:${secondsDisplay}`; + return `${minutesDisplay}:${secondsDisplay}.${millisecondsDisplay}`; } - return `${hours}:${minutesDisplay}:${secondsDisplay}`; + return `${hours}:${minutesDisplay}:${secondsDisplay}.${millisecondsDisplay}`; }; - const playerTimeDisplay = () => timeDisplay(playerTime()); - - const durationDisplay = () => timeDisplay(duration()); - return ( -
-
-
- {isPlaying() (props.mediaPlayer().paused = !props.mediaPlayer().paused)} - /> -
-
- {playerTimeDisplay()} / {durationDisplay()} -
-
-
- {isMuted() (props.mediaPlayer().muted = !props.mediaPlayer().muted)} - /> - { - const player = props.mediaPlayer(); - player.volume = event.offsetX / event.currentTarget.offsetWidth; - }} - /> -
-
- -
-
- -
-
- fullscreen { - const player = props.mediaPlayer(); - if (!player.state.canFullscreen) { - return; - } - if (isFullscreen()) { - player.exitFullscreen(); - } else { - player.requestFullscreen(); - } - }} - /> -
-
- + { - const player = props.mediaPlayer(); - const progressProportion = event.offsetX / event.currentTarget.offsetWidth; - const time = progressProportion * duration(); - player.currentTime = time; + const player = mediaPlayer(); + if (player.paused) { + player.play(event); + } else { + player.pause(event); + } }} /> -
+ + + + + + + + + + + + Play + Pause + + + + + + + + + + + + + Unmute + Mute + + + + +
+
+ + + +
+
+ +
+ {formatTime(playerTime())} + / + {formatTime(duration())} +
+ +
+ + + + + + + + + + Turn Closed Captions Off + Turn Closed Captions On + + + + + + + + + + + + Enter Fullscreen + Exit Fullscreen + + +
+ + + + + + + + + + +
+ ); }; diff --git a/thrimbletrimmer/src/globalStyle.scss b/thrimbletrimmer/src/globalStyle.scss index d4d196d..c40968a 100644 --- a/thrimbletrimmer/src/globalStyle.scss +++ b/thrimbletrimmer/src/globalStyle.scss @@ -43,10 +43,4 @@ a, .hidden { display: none; -} - -media-player, -video { - max-width: 100%; - max-height: 50vh; -} +} \ No newline at end of file diff --git a/thrimbletrimmer/src/images/video-controls/fullscreen.png b/thrimbletrimmer/src/images/video-controls/fullscreen.png deleted file mode 100644 index 664bc2a..0000000 Binary files a/thrimbletrimmer/src/images/video-controls/fullscreen.png and /dev/null differ diff --git a/thrimbletrimmer/src/images/video-controls/pause.png b/thrimbletrimmer/src/images/video-controls/pause.png deleted file mode 100644 index 8de238f..0000000 Binary files a/thrimbletrimmer/src/images/video-controls/pause.png and /dev/null differ diff --git a/thrimbletrimmer/src/images/video-controls/play.png b/thrimbletrimmer/src/images/video-controls/play.png deleted file mode 100644 index 30d233a..0000000 Binary files a/thrimbletrimmer/src/images/video-controls/play.png and /dev/null differ diff --git a/thrimbletrimmer/src/images/video-controls/volume-mute.png b/thrimbletrimmer/src/images/video-controls/volume-mute.png deleted file mode 100644 index 1f907e4..0000000 Binary files a/thrimbletrimmer/src/images/video-controls/volume-mute.png and /dev/null differ diff --git a/thrimbletrimmer/src/images/video-controls/volume.png b/thrimbletrimmer/src/images/video-controls/volume.png deleted file mode 100644 index aeb1254..0000000 Binary files a/thrimbletrimmer/src/images/video-controls/volume.png and /dev/null differ diff --git a/thrimbletrimmer/src/restreamer/Restreamer.tsx b/thrimbletrimmer/src/restreamer/Restreamer.tsx index 6a44d80..ff662e9 100644 --- a/thrimbletrimmer/src/restreamer/Restreamer.tsx +++ b/thrimbletrimmer/src/restreamer/Restreamer.tsx @@ -16,16 +16,9 @@ import { KeyboardShortcuts, StreamTimeSettings, StreamVideoInfo, - VideoControls, + VideoPlayer, } from "../common/video"; -import "vidstack/player/styles/default/theme.css"; -import "vidstack/player/styles/default/layouts/video.css"; -import "vidstack/player"; -import "vidstack/player/layouts/default"; -import "vidstack/player/ui"; -import { MediaPlayerElement } from "vidstack/elements"; - export interface DefaultsData { video_channel: string; bustime_start: string; @@ -99,12 +92,6 @@ const RestreamerWithDefaults: Component = (props) => { streamStartTime: DateTime.utc().minus({ minutes: 10 }), streamEndTime: null, }); - const [mediaPlayer, setMediaPlayer] = createSignal(); - - createEffect(() => { - const info = streamVideoInfo(); - console.log(info); - }); const videoURL = () => { const streamInfo = streamVideoInfo(); @@ -122,12 +109,6 @@ const RestreamerWithDefaults: Component = (props) => { return url; }; - createEffect(() => { - const player = mediaPlayer(); - const srcURL = videoURL(); - player.src = srcURL; - }); - return ( <> = (props) => { errorList={props.errorList} setErrorList={props.setErrorList} /> - - - - + ); };