Skip to content

Commit

Permalink
Merge pull request #28 from BKWLD/video-control
Browse files Browse the repository at this point in the history
Video control
  • Loading branch information
weotch authored Sep 22, 2023
2 parents 0dafaa8 + bfcfd72 commit 8fcc422
Show file tree
Hide file tree
Showing 15 changed files with 227 additions and 11 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
matrix:
dir:
- packages/next
- packages/react
- packages/sanity-next

steps:
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@ export default function Example({ background }) {

This project uses Lerna to release versions, using [the default versioning strategy](https://lerna.js.org/docs/features/version-and-publish#versioning-strategies).

- `yarn lerna run test` - Run all tests
- `yarn lerna version` - Tag a new version
- `yarn lerna publish from-package` - Publish versions to NPM
- `yarn lerna publish [major|minor|path]` - Tag and publish a version
- `yarn lerna publish from-package` - Publish the current version
2 changes: 1 addition & 1 deletion packages/next/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Cypress.Commands.add('hasDimensions',
Cypress.Commands.add('isPlaying',
{ prevSubject: true },
(subject) => {
cy.wrap(subject).should('have.prop', 'paused', true)
cy.wrap(subject).should('have.prop', 'paused', false)
})

// Add Typescript support for custom commaands
Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/NextVisual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function NextVisual(
priority,
sizes,
imageLoader,
playing,
alt,
className = '',
style = {},
Expand Down Expand Up @@ -67,7 +68,8 @@ export default function NextVisual(
fit,
position,
priority,
noPoster: !!image // Use `image` as poster frame
noPoster: !!image, // Use `image` as poster frame
playing,
}} /> }

</VisualWrapper>
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/types/nextVisualTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type NextVisualProps = {
sizes?: string
imageLoader?: Function

playing?: boolean

alt: string

className?: string
Expand Down
13 changes: 13 additions & 0 deletions packages/react/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from "cypress";

export default defineConfig({
viewportWidth: 500,
viewportHeight: 500,
component: {
specPattern: "cypress/component/**/*.cy.{js,jsx,ts,tsx}",
devServer: {
framework: "next",
bundler: "webpack",
},
},
});
42 changes: 42 additions & 0 deletions packages/react/cypress/component/LazyVideo.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState } from 'react'
import { LazyVideo } from '../../src'

// Make an instance of LazyVideo that can be controlled
const Player = function({ autoplay }: any) {

const [playing, setPlaying] = useState(autoplay)

return (<>
<LazyVideo
src='https://placehold.co/300x200.mp4'
alt=''
playing={ playing }/>
<div style={{ position: 'relative' }}>
<button onClick={() => setPlaying(true)}>Play</button>
<button onClick={() => setPlaying(false)}>Pause</button>
</div>
</>)
}


describe('playback', () => {

it('can be paused and then restarted', () => {
cy.mount(<Player autoplay={true} />)
cy.get('video').isPlaying()
cy.get('button').contains('Pause').click()
cy.get('video').isPaused()
cy.get('button').contains('Play').click()
cy.get('video').isPlaying()
})

it('can be started and then paused', () => {
cy.mount(<Player autoplay={false} />)
cy.get('video').isPaused()
cy.get('button').contains('Play').click()
cy.get('video').isPlaying()
cy.get('button').contains('Pause').click()
cy.get('video').isPaused()
})

})
37 changes: 37 additions & 0 deletions packages/react/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// <reference types="cypress" />

// Check that a video is playing
// https://glebbahmutov.com/blog/test-video-play/
Cypress.Commands.add('isPlaying',
{ prevSubject: true },
(subject) => {
cy.wrap(subject).should('have.prop', 'paused', false)
})

// Check that a video is playing
// https://glebbahmutov.com/blog/test-video-play/
Cypress.Commands.add('isPaused',
{ prevSubject: true },
(subject) => {
cy.wrap(subject).should('have.prop', 'paused', true)
})

// Add Typescript support for custom commaands
// https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands
export {};
declare global {
namespace Cypress {
interface Chainable {

hasDimensions(
width: number,
height: number
): Chainable<JQueryWithSelector>

isPlaying(): Chainable<void>
isPaused(): Chainable<void>
}
}
}


20 changes: 20 additions & 0 deletions packages/react/cypress/support/component-index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
<!-- Used by Next.js to inject CSS. -->
<div id="__next_css__DO_NOT_USE__"></div>
<style>
html, body {
padding: 0;
margin: 0;
}
</style>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
21 changes: 21 additions & 0 deletions packages/react/cypress/support/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')

import { mount } from 'cypress/react18'

// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}

Cypress.Commands.add('mount', mount)
18 changes: 18 additions & 0 deletions packages/react/cypress/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es5",
"jsx": "preserve",
"lib": [
"es5",
"dom"
],
"types": [
"cypress",
"node"
]
},
"include": [
"**/*.ts",
"**/*.tsx"
]
}
3 changes: 2 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"dist/**/*"
],
"scripts": {
"build": "rm -rf dist/* && tsc"
"build": "rm -rf dist/* && tsc",
"test": "cypress run --component"
},
"dependencies": {
"react-intersection-observer": "^9"
Expand Down
48 changes: 42 additions & 6 deletions packages/react/src/LazyVideo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,54 @@
"use client";

import { useInView } from 'react-intersection-observer'
import type { ReactElement } from 'react'
import { useEffect, type ReactElement, useRef, useCallback } from 'react'
import type { LazyVideoProps } from './types/lazyVideoTypes';

import { fillStyles, transparentGif } from './lib/styles'

// An video rendered within a Visual that supports lazy loading
export default function LazyVideo({
src, alt, fit, position, priority, noPoster
}: any): ReactElement {
src, alt, fit, position, priority, noPoster, playing = true
}: LazyVideoProps): ReactElement {

// Make a ref to the video so it can be controlled
const videoRef = useRef<HTMLVideoElement>()

// Watch for in viewport to load video unless using priority
const { ref, inView } = useInView({
const { ref: inViewRef, inView } = useInView({
skip: priority
})

// Support multiple refs on the video. This is from the
// react-intersection-observer docs
const setRefs = useCallback((node: HTMLVideoElement) => {
videoRef.current = node
inViewRef(node)
}, [inViewRef])

// Store the promise that is returned from play to prevent errors when
// pause() is called while video is benginning to play.
const playPromise = useRef<Promise<void>>()

// Play the video, waiting until it's safe to play it. And capture any
// errors while trying to play.
const play = async () => {
if (playPromise.current) await playPromise.current
try { playPromise.current = videoRef.current?.play()}
catch (e) { console.error(e) }
}

// Pause the video, waiting until it's safe to play it
const pause = async () => {
if (playPromise.current) await playPromise.current
videoRef.current?.pause()
}

// Respond to playing prop and call methods that control the video playback
useEffect(() => {
playing ? play() : pause()
}, [ playing ])

// Simplify logic for whether to load sources
const shouldLoad = priority || inView

Expand All @@ -24,17 +58,19 @@ export default function LazyVideo({

// Props that allow us to autoplay videos like a gif
playsInline
autoPlay
muted
loop

// Whether to autoplay
autoPlay={ playing }

// Load a transparent gif as a poster if an `image` was specified so
// the image is used as poster rather than the first frame of video. This
// lets us all use responsive poster images (via `next/image`).
poster={ noPoster ? transparentGif : undefined }

// Straightforward props
ref={ref}
ref={setRefs}
preload={ shouldLoad ? 'auto' : 'none' }
aria-label={ alt }
style={{
Expand Down
21 changes: 21 additions & 0 deletions packages/react/src/types/lazyVideoTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { CSSProperties } from 'react'

export type LazyVideoProps = {

// Source props
src: HTMLVideoElement['src']
alt: string

// Don't lazy load
priority?: boolean

// Use a transparent gif poster image
noPoster?: boolean

// Controls autoplaying and current playing state
playing?: boolean

// Display props
fit?: CSSProperties['objectFit']
position?: CSSProperties['objectPosition']
}
2 changes: 1 addition & 1 deletion packages/sanity-next/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Cypress.Commands.add('hasDimensions',
Cypress.Commands.add('isPlaying',
{ prevSubject: true },
(subject) => {
cy.wrap(subject).should('have.prop', 'paused', true)
cy.wrap(subject).should('have.prop', 'paused', false)
})

// Add Typescript support for custom commaands
Expand Down

0 comments on commit 8fcc422

Please sign in to comment.