Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Classifier: Custom Video Controls #3684

Merged
merged 41 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
838c5eb
Prepare SingleVideoViewer for adding unit tests
goplayoutside3 Sep 12, 2022
f2e15e5
clean up a bit
goplayoutside3 Sep 12, 2022
5ad95ce
create default storybook for SingleVideoViewerContainer
goplayoutside3 Sep 13, 2022
2f61ab3
working toward handling loading states
goplayoutside3 Sep 13, 2022
6d0d984
more renaming for clarity
goplayoutside3 Sep 13, 2022
0f1feb1
refactor SingleVideoViewerContainer as a functional component
goplayoutside3 Sep 13, 2022
d49c09e
add translation to SubjectViewer and unit tests for SingleVideoViewer…
goplayoutside3 Sep 13, 2022
6735ede
add SingleVideoViewer unit test
goplayoutside3 Sep 13, 2022
7be841c
clean up SingleVideoViewerContainer's readme
goplayoutside3 Sep 13, 2022
9458a80
use ternary for videoSrc in SingleVideoViewerContainer
goplayoutside3 Sep 15, 2022
36c692a
only render the drawing tools interaction layers when enableDrawing i…
goplayoutside3 Sep 15, 2022
fec8ad3
Remove FormattedTime and defaultProps
goplayoutside3 Sep 15, 2022
c5f371a
combine Slider component into VideoController file
goplayoutside3 Sep 15, 2022
610c073
Working toward imporoving VideoController performance
goplayoutside3 Sep 16, 2022
249a303
memoize ReactPlayer for slider performance
goplayoutside3 Sep 16, 2022
96f36b8
clean up formatTimeStamp and its unit tests
goplayoutside3 Sep 16, 2022
720cae4
getFixedNumber unit tests and docs
goplayoutside3 Sep 16, 2022
8b9af19
handle invalid inputs to getFixedNumber() and formatTimeStamp()
goplayoutside3 Sep 16, 2022
d9e5729
handle playback rate label
goplayoutside3 Sep 16, 2022
e8cc4d6
add unit tests for VideoController
goplayoutside3 Sep 19, 2022
5d62695
create custom theme for VideoController
goplayoutside3 Sep 19, 2022
f8454ae
add volume and fullscreen controls
goplayoutside3 Sep 19, 2022
73dd3c0
build custom volume range input
goplayoutside3 Sep 20, 2022
49261f6
fix grid styling of video controls
goplayoutside3 Sep 20, 2022
1366596
clean up and add more unit tests
goplayoutside3 Sep 20, 2022
50d9211
Merge branch 'master' into single-video-viewer-upgrade
goplayoutside3 Sep 20, 2022
e7d5caf
Merge branch 'single-video-viewer-upgrade'
goplayoutside3 Sep 20, 2022
06b7480
fix typo
goplayoutside3 Sep 20, 2022
940f707
Merge branch 'master'
goplayoutside3 Sep 22, 2022
33d0fc0
clean up merge from 'master'
goplayoutside3 Sep 22, 2022
16ded41
Merge branch 'master'
goplayoutside3 Sep 26, 2022
0e32db1
fix missing import
goplayoutside3 Sep 26, 2022
6652812
Merge branch 'master' into video-controller-refactor
goplayoutside3 Oct 10, 2022
afc2bfe
fix small typo in SingleVideoViewer's README
goplayoutside3 Oct 10, 2022
c8be69a
implement change suggestions from PR review
goplayoutside3 Oct 10, 2022
a5e3544
Update README of formatTimeStamp()
goplayoutside3 Oct 10, 2022
f6902d7
update grammar of formatTimeStamp() README
goplayoutside3 Oct 10, 2022
47420a0
add ADR 45 + custom controls only display with drawing tools enabled
goplayoutside3 Oct 10, 2022
134c203
add more stories and unit tests to SingleVideoViewerContainer
goplayoutside3 Oct 10, 2022
ae7d50f
Resolve package.json conflict with master
goplayoutside3 Oct 10, 2022
191fcad
Merge branch 'master' into video-controller-refactor
goplayoutside3 Oct 10, 2022
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
20 changes: 20 additions & 0 deletions docs/arch/adr-45.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ADR 45: Custom Video Controls

10 Oct 2022

## Context
The video subject viewer was built as part of the migration of projects with video subjects from PFE to FEM. In PFE, video subjects are displayed with native browser controls plus custom playback speed buttons.

In FEM, custom video controls are planned for building drawing tools layers on top of a video subject. The `react-player` used to display a video subject has built-in (native browser) controls, but their position overlaps some of the video. When an svg is placed on top of the player to record annotations, the built-in controls become unusable, hence the need for custom controls displayed below the subject.

## Decision
The end goal for the video subject viewer is to always display custom video controls for consistency - regardless if a project uses drawing tools with a video subject. However, migration of projects from PFE to FEM requires seemlessly moving video subjects to FEM's video subject viewer. There are no launched projects that use video subjects + drawing tools and the custom controls are incomplete. Therefore, further development of custom video controls will be paused and FEM's video subject viewer will use the same native browser controls as PFE for already-launched non-drawing-tools projects.

See [PR 3684](https://github.com/zooniverse/front-end-monorepo/pull/3684) for more discussion.

## Consequences
The video subject viewer will be deployed without custom video controls for now. Future work toward projects with video subjects + drawing tools is tracked in the [Video Annotations](https://github.com/zooniverse/front-end-monorepo/projects/13) project board.

## Status
Accepted

1 change: 1 addition & 0 deletions packages/lib-classifier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@storybook/builder-webpack5": "~6.5.0",
"@storybook/manager-webpack5": "~6.5.0",
"@storybook/react": "~6.5.0",
"@storybook/testing-react": "~1.3.0",
"@testing-library/dom": "~8.19.0",
"@testing-library/react": "~12.1.2",
"@testing-library/react-hooks": "~8.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The only allowed video media type is mp4.

## Features

SingleVideoViewerContainer handles state for SingleVideoViewer and VideoController. It's also contains an svg and InteractionLayer for drawing on a video subject.
SingleVideoViewerContainer handles state for the `react-player` component and VideoController. It also contains an svg and InteractionLayer for drawing on a video subject.

Refs
- `interactionLayerSVG`: Reference to svg element displayed on top of video subject. Needed for drawing tools' InteractionLayer.
Expand All @@ -24,12 +24,14 @@ Props
State Variables
- `clientWidth`: (number) Returned from `getBoundingClientRect()` on the <video> element in `react-player`.
- `duration`: (number) Duration of the video subject. Seconds rounded to 3 decimal places.
- `fullscreen`: (boolean) Whether or not the video is displayed fullscreen.
- `isPlaying`: (boolean) Whether or not the video subject is playing.
- `isSeeking`: (boolean) Whether or not a user is interacting with the VideoController > Slider.
- `playbackRate`: (number) 1, 0.5, or 0.25 ratio determines the speed of video playback.
- `timeStamp`: (number) Current played timestamp of video subject.
- `playbackSpeed`: (string) 1x, 0.5x, or 0.25x ratio determines the speed of video playback.
- `timeStamp`: (number) Represented by percent of subject played (0 to 1).
- `videoHeight`: (number) Natural height of video subject file.
- `videoWidth`: (number) Natural width of the video subject file.
- `volume`: (number) Number between 0 and 1. It's passed to the react-player to control volume.
- `volumeOpen`: (boolean) Determines whether the VideoController's volume range input is displayed or not.

## External Setup: Workflows and Subjects

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import PropTypes from 'prop-types'
import styled from 'styled-components'
import { Box } from 'grommet'
import { MobXProviderContext } from 'mobx-react'
import React, { useContext, useState, useRef } from 'react'
import React, { useContext, useMemo, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import asyncStates from '@zooniverse/async-states'
import ReactPlayer from 'react-player'

import SVGContext from '@plugins/drawingTools/shared/SVGContext'

import getFixedNumber from '../../helpers/getFixedNumber'
import InteractionLayer from '../InteractionLayer'
import locationValidator from '../../helpers/locationValidator'
import SingleVideoViewer from './SingleVideoViewer'
import VideoController from '../VideoController/VideoController'
import getFixedNumber from '../../helpers/getFixedNumber'
import VideoController from './components/VideoController'

const SubjectContainer = styled.div`
position: relative;
Expand Down Expand Up @@ -40,12 +40,14 @@ function SingleVideoViewerContainer({
}) {
const [clientWidth, setClientWidth] = useState(0)
const [duration, setDuration] = useState(0)
const [fullscreen, setFullscreen] = useState(false)
const [isPlaying, setIsPlaying] = useState(false)
const [isSeeking, setIsSeeking] = useState(false)
const [playbackRate, setPlaybackRate] = useState(1)
const [playbackSpeed, setPlaybackSpeed] = useState('1x')
const [timeStamp, setTimeStamp] = useState(0)
const [videoHeight, setVideoHeight] = useState(0)
const [videoWidth, setVideoWidth] = useState(0)
const [volume, setVolume] = useState(1)
const [volumeOpen, toggleVolumeOpen] = useState(false)

const interactionLayerSVG = useRef()
const playerRef = useRef()
Expand Down Expand Up @@ -83,17 +85,14 @@ function SingleVideoViewerContainer({
}
}

const enableDrawing = loadingState === asyncStates.success && enableInteractionLayer
const enableDrawing = enableInteractionLayer && loadingState === asyncStates.success

/* ==================== SingleVideoViewer react-player ==================== */
/* ==================== react-player ==================== */

const handleVideoProgress = reactPlayerState => {
const { played } = reactPlayerState
const fixedNumber = getFixedNumber(played, 5)
// TO DO: Why wouldn't you set timestamp when seeking?
if (!isSeeking) {
setTimeStamp(fixedNumber)
}
const { played } = reactPlayerState // percentage of video played (0 to 1)
const fixedNumber = getFixedNumber(played, 3)
setTimeStamp(fixedNumber)
}

const handleVideoDuration = duration => {
Expand All @@ -109,29 +108,79 @@ function SingleVideoViewerContainer({
setIsPlaying(!prevStatePlaying)
}

const handleSpeedChange = rate => {
setPlaybackRate(rate)
const handleSpeedChange = speed => {
setPlaybackSpeed(speed)
}

const handleSliderMouseUp = () => {
setIsSeeking(false)
const handleSliderChange = e => {
const newTimeStamp = e.target.value
playerRef?.current?.seekTo(newTimeStamp, 'seconds')
}

const handleSliderMouseDown = () => {
setIsPlaying(false)
setIsSeeking(true)
const handleVolume = e => {
setVolume(e.target.value)
}

/* When VideoController > Slider is clicked or scrubbed */
const handleSliderChange = e => {
const played = getFixedNumber(e.target.value, 5)
playerRef?.current.seekTo(played)
const handleVolumeOpen = () => {
const prevVolumeOpen = volumeOpen
toggleVolumeOpen(!prevVolumeOpen)
}

const handleFullscreen = () => {
if (fullscreen) {
try {
document.exitFullscreen()
setFullscreen(false)
} catch (error) {
console.log(error)
}
} else {
try {
playerRef.current?.getInternalPlayer().requestFullscreen()
setFullscreen(true)
} catch (error) {
console.log(error)
}
}
}

const handlePlayerError = (error) => {
onError(error)
}

const sanitizedSpeed = Number(playbackSpeed.slice(0, -1))

/* Memoized so onProgress() and setTimeStamp() don't trigger each other */
const memoizedViewer = useMemo(() => (
<ReactPlayer
controls={!enableDrawing}
height='100%'
onDuration={handleVideoDuration}
onEnded={handleVideoEnded}
onError={handlePlayerError}
onReady={onReactPlayerReady}
onProgress={handleVideoProgress}
playing={isPlaying}
playbackSpeed={sanitizedSpeed}
progressInterval={100} // milliseconds
ref={playerRef}
width='100%'
volume={volume}
url={videoSrc}
config={{
file: { // styling the <video> element
attributes: {
style: {
display: 'block',
height: '100%',
width: '100%'
}
}
}
}}
/>
), [enableDrawing, isPlaying, playbackSpeed, videoSrc, volume])

const canvas = transformLayer?.current
const interactionLayerScale = clientWidth / videoWidth
const svgStyle = {}
Expand All @@ -144,17 +193,7 @@ function SingleVideoViewerContainer({
{videoSrc
? (
<SubjectContainer>
<SingleVideoViewer
isPlaying={isPlaying}
onDuration={handleVideoDuration}
onEnded={handleVideoEnded}
onError={handlePlayerError}
onReactPlayerReady={onReactPlayerReady}
onProgress={handleVideoProgress}
playbackRate={playbackRate}
playerRef={playerRef}
url={videoSrc}
/>
{memoizedViewer}
{enableDrawing && (
<DrawingLayer>
<Box overflow='hidden'>
Expand Down Expand Up @@ -187,17 +226,25 @@ function SingleVideoViewerContainer({
: (
<Box>{t('SubjectViewer.SingleVideoViewerContainer.error')}</Box>
)}
<VideoController
isPlaying={isPlaying}
played={timeStamp}
playbackRate={playbackRate}
duration={duration}
onPlayPause={handlePlayPause}
onSpeedChange={handleSpeedChange}
onSliderMouseUp={handleSliderMouseUp}
onSliderMouseDown={handleSliderMouseDown}
onSliderChange={handleSliderChange}
/>

{/** See ADR 45 for notes on custom video controls */}
{enableDrawing && (
<VideoController
duration={duration}
enableDrawing={enableDrawing}
isPlaying={isPlaying}
handleFullscreen={handleFullscreen}
handleVolumeOpen={handleVolumeOpen}
onPlayPause={handlePlayPause}
onSliderChange={handleSliderChange}
onSpeedChange={handleSpeedChange}
onVolumeChange={handleVolume}
playbackSpeed={playbackSpeed}
timeStamp={timeStamp}
volume={volume}
volumeOpen={volumeOpen}
/>
)}
</>
)
}
Expand Down
Loading