Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ec63dde
fix: update timeIndicator subcomponent
kaseyvee Apr 10, 2024
11e3069
refactor: update controls to work with video
kaseyvee Apr 10, 2024
24a653a
refactor: update media types
kaseyvee Apr 10, 2024
5f5a820
feat(video): create video component
kaseyvee Apr 10, 2024
f36bfc9
docs(video): add video stories
kaseyvee Apr 10, 2024
746cbb5
test(video): add video tests
kaseyvee Apr 10, 2024
00d2c63
refactor: update captions toggle and create generic toggle interface
kaseyvee Apr 10, 2024
cd42d3a
refactor: separate controls into different files
kaseyvee Apr 10, 2024
f2ebb3c
fix: update mobile view
kaseyvee Apr 10, 2024
8c10bfd
refactor: update fullscreen button function and update tests
kaseyvee Apr 10, 2024
f2415ee
feat: allow ability to view transript on fullscreen
kaseyvee Apr 10, 2024
bacba73
fix: update based on colour suggestion
kaseyvee Apr 11, 2024
2dd7274
refactor: add suggestions
kaseyvee Apr 11, 2024
c9cca0b
fix: add missing poster props and add back background
kaseyvee Apr 11, 2024
64e3230
docs(video): add story with poster prop
kaseyvee Apr 11, 2024
01d3fc2
fix: remove captions toggle component
kaseyvee Apr 11, 2024
361bcb2
test: update tests
kaseyvee Apr 11, 2024
2905cd0
fix: add video to exports
kaseyvee Apr 11, 2024
7461857
fix: visual bug when showing short transcripts on fullscreen
kaseyvee Apr 11, 2024
813a4be
fix: add background colour to controls
kaseyvee Apr 11, 2024
7d97130
fix: bug when entering fullscreen on mobile
kaseyvee Apr 12, 2024
fc6e818
refactor: replace missed backgroundColor with variable
kaseyvee Apr 12, 2024
37c9891
refactor: move video folder to media
kaseyvee Apr 12, 2024
69efd6c
fix: bug when switching between viewport sizes on fullscreen
kaseyvee Apr 12, 2024
edfa4d7
test: update tests
kaseyvee Apr 12, 2024
ac60180
ci: make cypress run with chrome
kaseyvee Apr 12, 2024
b52deca
docs: add video in quickstart example
kaseyvee Apr 12, 2024
5739f4b
refactor: move youtube back to original folder
kaseyvee Apr 12, 2024
c493dfa
refactor: add suggestions for handleVolumeChange
kaseyvee Apr 12, 2024
1afd19a
docs: add comments for event listeners
kaseyvee Apr 12, 2024
93dbc3e
test: update mediaIconButton test attributes
kaseyvee Apr 12, 2024
a5fa502
fix: add transcript and promise handling suggestions
kaseyvee Apr 13, 2024
b76f3b1
test: add test for fullscreen error
kaseyvee Apr 13, 2024
c985ed3
fix: add suggestions
kaseyvee Apr 15, 2024
e7fb9d7
fix: update variable names and show error descriptions
kaseyvee Apr 15, 2024
acb808b
feat: hide controls on mouse inactivity
kaseyvee Apr 15, 2024
608682a
feat: handle keyboard user inactivity
kaseyvee Apr 15, 2024
1bcfbb9
Apply suggestions from code review
kaseyvee Apr 15, 2024
e4c917d
docs: make captions story have smaller dimensions
kaseyvee Apr 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/validate-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ jobs:
uses: cypress-io/github-action@v6
with:
component: true
browser: chrome
4 changes: 3 additions & 1 deletion docs/quickStart.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Follow these steps to quickly integrate Rustic UI Components into your applicati
StreamingText,
Table,
Text,
Video,
YoutubeVideo,
} from '@rustic-ai/ui-components'

Expand All @@ -73,7 +74,8 @@ Follow these steps to quickly integrate Rustic UI Components into your applicati
table: Table,
calendar: FCCalendar,
codeSnippet: CodeSnippet,
audio: Sound
audio: Sound,
video: Video
}}
// Include other components as needed
/>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"scripts": {
"build": "webpack --config webpack.prod.config.js",
"setup-dev-env": "husky install",
"test": "LANG='en-US' TZ='America/Vancouver' cypress run --component",
"test": "LANG='en-US' TZ='America/Vancouver' cypress run --browser chrome --component",
"test:interactive": "LANG='en-US' TZ='America/Vancouver' cypress open",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
Expand Down
Binary file added public/videoExamples/videoCaptions.mp4
Binary file not shown.
Binary file added public/videoExamples/videoStorybook.mp4
Binary file not shown.
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import OpenLayersMap from './map/openLayersMap'
import MarkedMarkdown from './markdown/markedMarkdown'
import MarkedStreamingMarkdown from './markdown/markedStreamingMarkdown'
import Sound from './media/audio/sound'
import Video from './media/video/video'
import PopoverMenu from './menu/popoverMenu'
import MessageCanvas from './messageCanvas/messageCanvas'
import MessageSpace from './messageSpace/messageSpace'
Expand Down Expand Up @@ -39,6 +40,7 @@ export {
Text,
TextInput,
Timestamp,
Video,
YoutubeVideo,
}

Expand Down
8 changes: 4 additions & 4 deletions src/components/media/audio/sound.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Sound from './sound'

describe('Sound', () => {
const audioElement = '[data-cy=audio-element]'
const muteButton = '[data-cy=volumeUp-button]'
const muteButton = '[data-cy=mute-button]'
const playbackRateButton = '[data-cy=playback-rate-button]'
const volumeSlider = '[data-cy=volume-slider]'
const pauseButton = '[data-cy=pause-button]'
Expand Down Expand Up @@ -45,9 +45,9 @@ describe('Sound', () => {
})
it(`should go forwards and backwards 10 seconds when clicking the forward/back buttons on ${viewport} screen`, () => {
cy.viewport(viewport)
cy.get('[data-cy=forward-button]').click()
cy.get('[data-cy=forward-ten-seconds-button]').click()
cy.get(audioElement).its('0.currentTime').should('equal', 10)
cy.get('[data-cy=replay-button]').click()
cy.get('[data-cy=replay-ten-seconds-button]').click()
cy.get(audioElement).its('0.currentTime').should('equal', 0)
})
it(`should increase the playback speed when clicking the playback rate button then go back to 1x after 2x on ${viewport} screen`, () => {
Expand Down Expand Up @@ -95,7 +95,7 @@ describe('Sound', () => {
captions={captionsPath}
/>
)
cy.get('[data-cy=captions-toggle]').click()
cy.get('[data-cy=show-captions-button]').click()
cy.get('track').should('exist').should('have.attr', 'src', captionsPath)
})
it(`should display an error message when no valid sources are found on ${viewport} screen`, () => {
Expand Down
45 changes: 25 additions & 20 deletions src/components/media/audio/sound.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import './sound.css'

import ClosedCaptionDisabledRoundedIcon from '@mui/icons-material/ClosedCaptionDisabledRounded'
import ClosedCaptionRoundedIcon from '@mui/icons-material/ClosedCaptionRounded'
import { useMediaQuery, useTheme } from '@mui/material'
import Alert from '@mui/material/Alert'
import CircularProgress from '@mui/material/CircularProgress'
import IconButton from '@mui/material/IconButton'
import Fade from '@mui/material/Fade'
import Typography from '@mui/material/Typography'
import { Box } from '@mui/system'
import React, { useEffect, useRef, useState } from 'react'

import type { AudioFormat } from '../../types'
import {
MoveTenSecondsButton,
PausePlayToggle,
PlaybackRateButton,
PlayOrPauseButton,
ProgressSlider,
TranscriptToggle,
ToggleTranscriptButton,
VolumeSettings,
} from '../controls/controls'
} from '../controls/commonControls'
import { MediaIconButton } from '../controls/mediaIconButton'
import TimeIndicator from '../timeIndicator/timeIndicator'
import Transcript from '../transcript/transcript'

Expand All @@ -28,6 +27,7 @@ export default function Sound(props: AudioFormat) {
const [isLoading, setIsLoading] = useState(true)
const [elapsedTime, setElapsedTime] = useState(0)
const [errorMessage, setErrorMessage] = useState('')
const [controlErrorMessage, setControlErrorMessage] = useState('')

const theme = useTheme()
const audioRef = useRef<HTMLVideoElement>(null)
Expand Down Expand Up @@ -75,18 +75,13 @@ export default function Sound(props: AudioFormat) {

function renderCaptionsToggle() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function renderCaptionsToggle() {
function renderCaptionsButton() {

if (props.captions && props.captions.length > 0) {
const action = areCaptionsShown ? 'captionsOff' : 'captionsOn'

return (
<IconButton
<MediaIconButton
action={action}
onClick={() => setAreCaptionsShown(!areCaptionsShown)}
aria-label={areCaptionsShown ? 'Hide captions' : 'Show captions'}
data-cy="captions-toggle"
>
{areCaptionsShown ? (
<ClosedCaptionRoundedIcon color="primary" />
) : (
<ClosedCaptionDisabledRoundedIcon color="primary" />
)}
</IconButton>
/>
)
}
}
Expand All @@ -99,7 +94,10 @@ export default function Sound(props: AudioFormat) {
mediaElement={audioRef.current}
movement="replay"
/>
<PausePlayToggle mediaElement={audioRef.current} />
<PlayOrPauseButton
mediaElement={audioRef.current}
onError={setControlErrorMessage}
/>
<MoveTenSecondsButton
mediaElement={audioRef.current}
movement="forward"
Expand All @@ -118,9 +116,11 @@ export default function Sound(props: AudioFormat) {
function renderTranscriptToggle() {
if (props.transcript) {
return (
<TranscriptToggle
isTranscriptShown={isTranscriptShown}
setIsTranscriptShown={() => setIsTranscriptShown(!isTranscriptShown)}
<ToggleTranscriptButton
isTranscriptVisible={isTranscriptShown}
setIsTranscriptVisible={() =>
setIsTranscriptShown(!isTranscriptShown)
}
/>
)
}
Expand Down Expand Up @@ -234,6 +234,11 @@ export default function Sound(props: AudioFormat) {
<Box className="rustic-sound" data-cy="audio">
{renderVideoElement()}
{renderControls()}
<Fade in={controlErrorMessage.length > 0}>
<Alert severity="error" onClose={() => setControlErrorMessage('')}>
{controlErrorMessage}
</Alert>
</Fade>
</Box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
gap: 24px;
align-items: center;
position: relative;
width: 100%;
}

.rustic-progress-slider {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import './controls.css'
import './commonControls.css'

import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Icon from '@mui/material/Icon'
import IconButton from '@mui/material/IconButton'
import LinearProgress from '@mui/material/LinearProgress'
import Slider from '@mui/material/Slider'
import Typography from '@mui/material/Typography'
import React, { useState } from 'react'

import { formatDurationTime } from '../../helper'
import { MediaIconButton } from './mediaIconButton'

interface MediaIconButtonProps {
onClick: () => void
action: 'play' | 'pause' | 'forward' | 'replay' | 'volumeUp' | 'volumeOff'
className?: string
export interface MediaControls {
mediaElement: HTMLMediaElement
}

interface MediaControls {
mediaElement: HTMLMediaElement
interface ToggleTranscriptButtonProps {
isTranscriptVisible: boolean
setIsTranscriptVisible: () => void
}

interface TranscriptToggleProps {
isTranscriptShown: boolean
setIsTranscriptShown: () => void
interface PlayOrPauseButtonProps extends MediaControls {
onError: (errorMessage: string) => void
}

interface MoveTenSecondsButtonProps extends MediaControls {
Expand All @@ -34,31 +31,6 @@ interface MoveTenSecondsButtonProps extends MediaControls {

const percentMultiple = 100

export function MediaIconButton(props: MediaIconButtonProps) {
const controls = {
play: { symbol: 'play_circle', label: 'play' },
pause: { symbol: 'pause_circle', label: 'pause' },
forward: { symbol: 'forward_10', label: 'forward ten seconds' },
replay: { symbol: 'replay_10', label: 'replay ten seconds' },
volumeUp: { symbol: 'volume_up', label: 'mute' },
volumeOff: { symbol: 'volume_off', label: 'unmute' },
}
return (
<IconButton
onClick={props.onClick}
aria-label={`click to ${controls[props.action].label}`}
className={props.className}
data-cy={`${props.action}-button`}
>
<Icon color="primary">
<span className="material-symbols-rounded">
{controls[props.action].symbol}
</span>
</Icon>
</IconButton>
)
}

export function ProgressSlider(props: MediaControls) {
const formattedElapsedTime = formatDurationTime(
props.mediaElement.currentTime
Expand Down Expand Up @@ -116,30 +88,23 @@ export function ProgressSlider(props: MediaControls) {
}

export function VolumeSettings(props: MediaControls) {
const [volumeFraction, setVolumeFraction] = useState(1)
const [volumeFraction, setVolumeFraction] = useState(
props.mediaElement.volume
)
const [previousVolume, setPreviousVolume] = useState(
props.mediaElement.volume
)

const isMuted = props.mediaElement.muted
const action = isMuted ? 'volumeOff' : 'volumeUp'
const action = props.mediaElement.muted ? 'volumeOff' : 'volumeUp'

function handleMuteToggle() {
props.mediaElement.muted = !props.mediaElement.muted

props.mediaElement.onvolumechange = function () {
if (props.mediaElement.muted) {
setPreviousVolume(props.mediaElement.volume)
setVolumeFraction(0)
} else {
setVolumeFraction(props.mediaElement.volume)
}
}

function handleMuteToggle() {
if (isMuted && props.mediaElement.volume === 0) {
// If audio was muted and volume was 0, unmute and restore to full volume
props.mediaElement.muted = false
props.mediaElement.volume = 1
} else if (isMuted) {
// If audio was muted, unmute, restoring previous volume
props.mediaElement.muted = false
} else {
// If audio was unmuted, mute
props.mediaElement.muted = true
setVolumeFraction(previousVolume)
}
}

Expand All @@ -151,6 +116,7 @@ export function VolumeSettings(props: MediaControls) {

props.mediaElement.muted = updatedVolume === 0
props.mediaElement.volume = updatedVolume as number
setVolumeFraction(props.mediaElement.volume)
}

return (
Expand All @@ -175,55 +141,64 @@ export function VolumeSettings(props: MediaControls) {
)
}

export function TranscriptToggle(props: TranscriptToggleProps) {
const Icon = props.isTranscriptShown
export function ToggleTranscriptButton(props: ToggleTranscriptButtonProps) {
const Icon = props.isTranscriptVisible
? KeyboardArrowUpIcon
: KeyboardArrowDownIcon

const buttonText = `${props.isTranscriptShown ? 'Hide' : 'Show'} Transcript`
const buttonText = `${props.isTranscriptVisible ? 'Hide' : 'Show'} Transcript`

return (
<Button
className="rustic-transcript-toggle"
data-cy="transcript-toggle"
onClick={props.setIsTranscriptShown}
onClick={props.setIsTranscriptVisible}
endIcon={<Icon />}
>
<Typography variant="overline">{buttonText}</Typography>
</Button>
)
}

export function PausePlayToggle(props: MediaControls) {
const [isPlaying, setIsPlaying] = useState(false)
export function PlayOrPauseButton(props: PlayOrPauseButtonProps) {
const [isPlaying, setIsPlaying] = useState(!props.mediaElement.paused)

const action = isPlaying ? 'pause' : 'play'

function handlePausePlayToggle() {
function handlePlayOrPauseToggle() {
if (isPlaying) {
props.mediaElement.pause()
setIsPlaying(false)
} else {
props.mediaElement.play()
setIsPlaying(true)
props.mediaElement.play().catch((error: DOMException) => {
props.onError(`Failed to play the media. Error: ${error.message}`)
})
}
}

// State is updated by event listeners so that the icon is displayed correctly, even when play/pause is not initiated by the user or user initiates without direct use of this toggle (e.g. automatically pauses when picture-in-picture is exited, or pause and play can be toggled in the picture-in-picture window).
props.mediaElement.onended = function () {
setIsPlaying(false)
}
props.mediaElement.onpause = function () {
setIsPlaying(false)
}
props.mediaElement.onplay = function () {
setIsPlaying(true)
}

return (
<MediaIconButton
onClick={handlePausePlayToggle}
onClick={handlePlayOrPauseToggle}
action={action}
className="rustic-pause-play-icon"
/>
)
}

export function PlaybackRateButton(props: MediaControls) {
const [playbackRate, setPlaybackRate] = useState(1)
const [playbackRate, setPlaybackRate] = useState(
props.mediaElement.playbackRate
)

function handlePlaybackRateChange() {
let newPlaybackRate = 1
Expand All @@ -245,9 +220,7 @@ export function PlaybackRateButton(props: MediaControls) {
aria-label={`Playback rate: ${playbackRate}x, click to change`}
data-cy="playback-rate-button"
>
<Typography variant="body1" color="primary.main">
{playbackRate}X
</Typography>
<Typography variant="body1">{playbackRate}X</Typography>
</Button>
)
}
Expand Down
Loading