Skip to content

Commit

Permalink
fix: audio player async logic and auto pause logic
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Aug 21, 2024
1 parent 3fb8eac commit ba40b44
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 83 deletions.
60 changes: 33 additions & 27 deletions src/renderer/src/atoms/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ const patchedLocalStorage: SyncStorage<PlayerAtomValue> = {
export const [
,
,
usePlayerAtomValue,
useSetPlayerAtom,
getPlayerAtomValue,
setPlayerAtomValue,
usePlayerAtomSelector,
useAudioPlayerAtomValue,
useAudioSetPlayerAtom,
getAudioPlayerAtomValue,
setAudioPlayerAtomValue,
useAudioPlayerAtomSelector,
] = createAtomHooks<PlayerAtomValue>(
atomWithStorage(
getStorageNS("player"),
Expand All @@ -55,21 +55,23 @@ export const [
),
)

export const Player = {
export const AudioPlayer = {
audio: new Audio(),
currentTimeTimer: null as NodeJS.Timeout | null,

__currentActionId: 0,
get() {
return getPlayerAtomValue()
return getAudioPlayerAtomValue()
},
mount(
v: Omit<PlayerAtomValue, "show" | "status" | "playedSeconds" | "duration">,
) {
const curV = getPlayerAtomValue()
const curV = getAudioPlayerAtomValue()
if (!v.src || (curV.src === v.src && curV.status === "playing")) {
return
}

setPlayerAtomValue({
setAudioPlayerAtomValue({
...curV,
...v,
status: "loading",
Expand All @@ -85,15 +87,17 @@ export const Player = {

this.currentTimeTimer && clearInterval(this.currentTimeTimer)
this.currentTimeTimer = setInterval(() => {
setPlayerAtomValue({
...getPlayerAtomValue(),
setAudioPlayerAtomValue({
...getAudioPlayerAtomValue(),
currentTime: this.audio.currentTime,
})
}, 1000)

const currentActionId = this.__currentActionId
return this.audio.play().then(() => {
setPlayerAtomValue({
...getPlayerAtomValue(),
if (currentActionId !== this.__currentActionId) return
setAudioPlayerAtomValue({
...getAudioPlayerAtomValue(),
status: "playing",
duration: this.audio.duration,
})
Expand All @@ -104,17 +108,19 @@ export const Player = {
this.audio.pause()
},
play() {
const curV = getPlayerAtomValue()
++this.__currentActionId
const curV = getAudioPlayerAtomValue()

this.mount(curV)
},
pause() {
const curV = getPlayerAtomValue()
++this.__currentActionId
const curV = getAudioPlayerAtomValue()
if (curV.status === "paused") {
return
}

setPlayerAtomValue({
setAudioPlayerAtomValue({
...curV,
status: "paused",
currentTime: this.audio.currentTime,
Expand All @@ -123,7 +129,7 @@ export const Player = {
return
},
togglePlayAndPause() {
const curV = getPlayerAtomValue()
const curV = getAudioPlayerAtomValue()
if (curV.status === "playing") {
return this.pause()
} else if (curV.status === "paused") {
Expand All @@ -133,8 +139,8 @@ export const Player = {
}
},
close() {
setPlayerAtomValue({
...getPlayerAtomValue(),
setAudioPlayerAtomValue({
...getAudioPlayerAtomValue(),
show: false,
status: "paused",
})
Expand All @@ -143,15 +149,15 @@ export const Player = {
},
seek(time: number) {
this.audio.currentTime = time
setPlayerAtomValue({
...getPlayerAtomValue(),
setAudioPlayerAtomValue({
...getAudioPlayerAtomValue(),
currentTime: time,
})
},
setPlaybackRate(speed: number) {
this.audio.playbackRate = speed
setPlayerAtomValue({
...getPlayerAtomValue(),
setAudioPlayerAtomValue({
...getAudioPlayerAtomValue(),
playbackRate: speed,
})
},
Expand All @@ -163,15 +169,15 @@ export const Player = {
},
toggleMute() {
this.audio.muted = !this.audio.muted
setPlayerAtomValue({
...getPlayerAtomValue(),
setAudioPlayerAtomValue({
...getAudioPlayerAtomValue(),
isMute: this.audio.muted,
})
},
setVolume(volume: number) {
this.audio.volume = volume
setPlayerAtomValue({
...getPlayerAtomValue(),
setAudioPlayerAtomValue({
...getAudioPlayerAtomValue(),
volume,
})
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Player } from "@renderer/atoms/player"
import { AudioPlayer } from "@renderer/atoms/player"
import { nextFrame } from "@renderer/lib/dom"
import { useEntryContentContext } from "@renderer/modules/entry-content/hooks"

Expand All @@ -12,13 +12,13 @@ export const TimeStamp = (props: { time: string }) => {
<span
className="cursor-pointer tabular-nums text-accent dark:text-theme-accent-500"
onClick={() => {
Player.mount({
AudioPlayer.mount({
type: "audio",
entryId,
src,
currentTime: 0,
})
nextFrame(() => Player.seek(timeStringToSeconds(props.time) || 0))
nextFrame(() => AudioPlayer.seek(timeStringToSeconds(props.time) || 0))
}}
>
{props.time}
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/components/ui/media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ const VideoPreview: FC<{
/>
) : (
<VideoPlayer
variant="preview"
controls={false}
src={src}
ref={setVideoRef}
Expand Down
71 changes: 64 additions & 7 deletions src/renderer/src/components/ui/media/VideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as Slider from "@radix-ui/react-slider"
import { AudioPlayer } from "@renderer/atoms/player"
import { IconScaleTransition } from "@renderer/components/ux/transition/icon"
import { useRefValue } from "@renderer/hooks/common"
import type { HTMLMediaState } from "@renderer/hooks/common/factory/createHTMLMediaHook"
import { useVideo } from "@renderer/hooks/common/useVideo"
import { nextFrame, stopPropagation } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { useSingleton } from "foxact/use-singleton"
import { m, useDragControls, useSpring } from "framer-motion"
import type { PropsWithChildren } from "react"
import {
Expand All @@ -25,13 +27,16 @@ import {
} from "use-context-selector"
import { useEventCallback } from "usehooks-ts"

import { MotionButtonBase } from "../button"
import { softSpringPreset } from "../constants/spring"
import { KbdCombined } from "../kbd/Kbd"
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"
import { VolumeSlider } from "./VolumeSlider"

type VideoPlayerProps = {
src: string

variant?: "preview" | "player"
} & React.VideoHTMLAttributes<HTMLVideoElement> &
PropsWithChildren
export type VideoPlayerRef = {
Expand All @@ -55,18 +60,20 @@ interface VideoPlayerContextValue {
controls: VideoPlayerRef["controls"]
wrapperRef: React.RefObject<HTMLDivElement>
src: string
variant: "preview" | "player"
}
const VideoPlayerContext = createContext<VideoPlayerContextValue>(null!)
export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
({ src, className, ...rest }, ref) => {
({ src, className, variant = "player", ...rest }, ref) => {
const isPlayer = variant === "player"
const [clickToStatus, setClickToStatus] = useState(
null as "play" | "pause" | null,
)

const scaleValue = useSpring(1, softSpringPreset)
const opacityValue = useSpring(0, softSpringPreset)
const handleClick = useEventCallback((e?: any) => {
if (!rest.controls) return
if (!isPlayer) return
e?.stopPropagation()

if (state.playing) {
Expand Down Expand Up @@ -96,9 +103,10 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
rest.onClick?.(e)
handleClick(e)
},
muted: isPlayer ? false : true,
onDoubleClick(e) {
rest.onDoubleClick?.(e)
if (!rest.controls) return
if (!isPlayer) return
e.preventDefault()
e.stopPropagation()
if (!document.fullscreenElement) {
Expand Down Expand Up @@ -130,7 +138,7 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
)

return (
<div className="center relative size-full" ref={wrapperRef}>
<div className="group center relative size-full" ref={wrapperRef}>
{element}

<div className="center pointer-events-none absolute inset-0">
Expand All @@ -149,18 +157,67 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
</m.div>
</div>

{state.hasAudio && !state.muted && state.playing && (
<BizControlOutsideMedia />
)}

<VideoPlayerContext.Provider
value={useMemo(
() => ({ state, controls, wrapperRef, src }),
[state, controls, src],
() => ({ state, controls, wrapperRef, src, variant }),
[state, controls, src, variant],
)}
>
{rest.controls && <ControlBar />}
{variant === "preview" && <FloatMutedButton />}
{isPlayer && <ControlBar />}
</VideoPlayerContext.Provider>
</div>
)
},
)
const BizControlOutsideMedia = () => {
const currentAudioPlayerIsPlayRef = useSingleton(
() => AudioPlayer.get().status === "playing",
)
useEffect(() => {
const { current } = currentAudioPlayerIsPlayRef
if (current) {
AudioPlayer.pause()
}

return () => {
if (current) {
AudioPlayer.play()
}
}
}, [currentAudioPlayerIsPlayRef])

return null
}

const FloatMutedButton = () => {
const ctx = useContext(VideoPlayerContext)
const isMuted = ctx.state.muted
return (
<MotionButtonBase
className="center absolute right-4 top-4 z-10 size-7 rounded-full bg-black/50 opacity-0 duration-200 group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation()
if (isMuted) {
ctx.controls.unmute()
} else {
ctx.controls.mute()
}
}}
>
<IconScaleTransition
className="size-4 text-white"
icon1="i-mgc-volume-cute-re"
icon2="i-mgc-volume-mute-cute-re"
status={isMuted ? "done" : "init"}
/>
</MotionButtonBase>
)
}

const ControlBar = memo(() => {
const controls = useContextSelector(VideoPlayerContext, (v) => v.controls)
Expand Down
12 changes: 10 additions & 2 deletions src/renderer/src/components/ux/transition/icon.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { cn } from "@renderer/lib/utils"
import { AnimatePresence, m } from "framer-motion"
import { useEffect, useState } from "react"

export const IconScaleTransition = ({
icon1,
icon2,
status,

className,
icon1ClassName,
icon2ClassName,
}: {
status: "init" | "done"

icon1: string
icon1ClassName?: string
icon2: string
icon2ClassName?: string
className?: string
}) => {
const [isMount, isMounted] = useState(false)
useEffect(() => {
Expand All @@ -24,15 +32,15 @@ export const IconScaleTransition = ({
<AnimatePresence mode="popLayout">
{status === "init" ? (
<m.i
className={icon1}
className={cn(icon1ClassName, className, icon1)}
key="1"
initial={initial}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
/>
) : (
<m.i
className={icon2}
className={cn(icon2ClassName, className, icon2)}
key="2"
initial={initial}
animate={{ scale: 1 }}
Expand Down
Loading

0 comments on commit ba40b44

Please sign in to comment.