From 2095880d102ff281002f980b4bac98848eb0cd2b Mon Sep 17 00:00:00 2001 From: ElementalAlchemist Date: Thu, 14 Nov 2024 02:21:54 -0600 Subject: [PATCH] Switch to more native video controls --- thrimbletrimmer/package.json | 1 + thrimbletrimmer/src/common/video.module.scss | 47 --- thrimbletrimmer/src/common/video.scss | 29 ++ thrimbletrimmer/src/common/video.tsx | 279 +++++++++--------- thrimbletrimmer/src/globalStyle.scss | 8 +- .../src/images/video-controls/fullscreen.png | Bin 633 -> 0 bytes .../src/images/video-controls/pause.png | Bin 600 -> 0 bytes .../src/images/video-controls/play.png | Bin 667 -> 0 bytes .../src/images/video-controls/volume-mute.png | Bin 756 -> 0 bytes .../src/images/video-controls/volume.png | Bin 747 -> 0 bytes thrimbletrimmer/src/restreamer/Restreamer.tsx | 26 +- 11 files changed, 172 insertions(+), 218 deletions(-) create mode 100644 thrimbletrimmer/src/common/video.scss delete mode 100644 thrimbletrimmer/src/images/video-controls/fullscreen.png delete mode 100644 thrimbletrimmer/src/images/video-controls/pause.png delete mode 100644 thrimbletrimmer/src/images/video-controls/play.png delete mode 100644 thrimbletrimmer/src/images/video-controls/volume-mute.png delete mode 100644 thrimbletrimmer/src/images/video-controls/volume.png 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 664bc2a31e05f614ba8f6eaedb17a057f7aec9e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 633 zcmV-<0*3vGP)EX>4Tx04R}tkv&MmKpe$iQ?;TM5j%)DWT;LSL`5963Pq?8YK2xEOfLNpnlvOS zE{=k0!NHHks)LKOt`4q(Aou~|=H{g6A|?JWDYS_3;J6>}?mh0_0Ya_BG^=e4&~)2O zCE{WxyCQ~O(Ty-V5JI2KEMr!ZlJFg0_XzOyF2=L`&;2=i)SShDfJi*U4AUlFC!X50 z4bJ<-5muB{;&b9rlP*a7$aTfzH_io@1)do()2TV)2(egbVWovx(bR}1iKD8fQ@)V# zSmnIMSu0goAMvPXS6bmWZkNfxsUB5&wg`Uwzx2Cnp`zgz>RKS{4P zwdfJhyA51iH#KDsxZD8-o($QPT`5RY$mfCgGy0}1(0>bbt$MvR_Hp_Eq^Yaq4RCM> zj1(w)&F9^nt-bwwrqSOI@jh~M85YVL00006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{002=*L_t(Y$L*9c4geqs1FPX{ytrmY`P+KZT^Nsu%I!UsKsdkNl!Kv?7NAGwmSO T?#<3900000NkvXXu0mjfMqLRa diff --git a/thrimbletrimmer/src/images/video-controls/pause.png b/thrimbletrimmer/src/images/video-controls/pause.png deleted file mode 100644 index 8de238f044208ef9521d95c09884402b23de5520..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 600 zcmV-e0;m0nP)EX>4Tx04R}tkv&MmKp2MKrbz@3D;ex)r#C4j(NMQkskRU=q4HZ;jBTl=bb^8^S!16O+6ztI4uKS{5* zwb&6bunk;Xw>4!CxZD8-pA6ZQT`5RYC>DYDGy0}H5V-|eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{001yaL_t(Y$75g^1p^Hj(FJzz-u)k&LRVK;Msn0K(b@;2 m7K~ajYQd-lqZW*U;Q;{Qi3lLkiMJvE0000EX>4Tx04R}tkv&MmKp2MKrbz@3D;ex)r#C4j(NMQkskRU=q4HZ;jBTl=bb^8^S!16O+6ztI4uKS{5* zwb&6bunk;Xw>4!CxZD8-pA6ZQT`5RYC>DYDGy0}H5V-|hk5?EC-#02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{0047IL_t(Y$L-ZY3V<*S1i)4DH1}QluXriwyY%8wO4B4D z(mmz01Eo!Yk^F`lc7)dd3sLMsm*r5COFSF319OK|O#8@&HXx1W<+M>~|#r z9n~X}tYQyfx~7sEX>4Tx04R}tkv&MmKp2MKrbz@3D;ex)r#C4j(NMQkskRU=q4HZ;jBTl=bb^8^S!16O+6ztI4uKS{5* zwb&6bunk;Xw>4!CxZD8-pA6ZQT`5RYC>DYDGy0}H5V-|eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{007KML_t(Y$L*Cp3c^4Tg}=>7;w7vUM0ll@BIG36X{jd> zL>t=yv9R(Kxrc=|5oFbv%_i9R%Gu?8v-88kUv0_EmR0U05$20l03qhVfKc-wKppeS zfHHG4OUq);JGccV*a0rURMM&N7yw(KD=CixNOES@1NJ}$T!9^sNjlaZQx9?m#)a|W z*}M_JyHZClnpf6-k^KZ10UI+L{-((#S#OeNOk9AD(At;)0000EX>4Tx04R}tkv&MmKp2MKrbz@3D;ex)r#C4j(NMQkskRU=q4HZ;jBTl=bb^8^S!16O+6ztI4uKS{5* zwb&6bunk;Xw>4!CxZD8-pA6ZQT`5RYC>DYDGy0}H5V-|eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{006^DL_t(Y$L*D|4Z<)GMPI04m{icF_;r>+f(}Ypfg(FV zdq{MQz!nrt08J91$b>kyyL`#gFHZm2x;q&rrIMNLht$mi!gomkKH=T~-*7L$nDD`X zzHl>Jx_qOoQ~dV4pnwX<>r(-_q-W3+wdv9oI0Nf%A3UZVoq*EeqXAyP+~K1EO5pDB zQJTC0EI3b0Nd>w(MhO4_002ovPDHLkV1lW_Ml=8b 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} /> - - - - + ); };