Skip to content

Instantly share code, notes, and snippets.

@ndugger
Created March 8, 2021 05:03
Show Gist options
  • Save ndugger/c7ab88559d98985fd813ea695d55baeb to your computer and use it in GitHub Desktop.
Save ndugger/c7ab88559d98985fd813ea695d55baeb to your computer and use it in GitHub Desktop.
import { Component, createElement } from 'cortex'
import { createStyleSheet, selector } from 'cortex-css'
import { DateTime } from 'luxon'
import { Theme } from '~/contexts/Theme'
import { Button } from './Button'
import { Container } from './Container'
import { Divider } from './Divider'
import { Flexbox } from './Flexbox'
import { Icon } from './Icon'
import { Spacer } from './Spacer'
import { Typography } from './Typography'
export class VideoPlayer extends Component {
private playing = false
private timer = -1
public poster = ''
public src = ''
private get media() {
return this.shadowRoot?.querySelector(selector.selectClass(HTMLVideoElement)) as HTMLVideoElement
}
protected handleFullscreenClick() {
this.requestFullscreen()
}
protected handleLoadedData() {
this.update()
}
protected handlePicInPicClick() {
(this.media as any).requestPictureInPicture()
}
protected handlePlay() {
this.update({
playing: true
}).then(() => {
this.update({
timer: window.setInterval(() => {
this.update()
}, 100)
})
})
}
protected handlePause() {
window.clearInterval(this.timer)
this.update({
playing: false,
timer: -1
})
}
protected handlePlayPauseClick() {
if (this.playing) {
this.media.pause()
}
else {
this.media.play()
}
}
protected handleSeekClick(event: MouseEvent) {
const x = (event as any).layerX
const width = (event.currentTarget as HTMLDivElement).offsetWidth
this.media.currentTime = this.media.duration * (x / width)
this.update()
}
protected render() {
if (this.src.includes('youtu.be') || this.src.includes('youtube.com')) return [
<HTMLIFrameElement src={ this.src } allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowFullscreen frameBorder='0'/>
]
if (!this.media) return [
<HTMLVideoElement src={ this.src }/>
]
const buffered = DateTime.fromSeconds(this.media.buffered.length ? this.media.buffered.end(this.media.buffered.length - 1) : 0)
const currentTime = DateTime.fromSeconds(this.media.currentTime || 0)
const duration = DateTime.fromSeconds(this.media.duration || 0)
const theme = this.getContext(Theme)
return [
<HTMLVideoElement onclick={ () => this.handlePlayPauseClick() } onloadeddata={ () => this.handleLoadedData() } onplay={ () => this.handlePlay() } onpause={ () => this.handlePause() } src={ this.src }/>,
(!this.playing && (currentTime.toSeconds() === 0 || currentTime.equals(duration))) && (
<Flexbox align={ Flexbox.Alignment.Center } id='action' justify={ Flexbox.Alignment.Center } onclick={ () => this.handlePlayPauseClick() }>
<Container>
<Icon color={ theme?.color?.secondary } glyph={ currentTime.equals(duration) ? 'repeat-line' : 'play-fill' } size={ 40 }/>
</Container>
</Flexbox>
),
<Divider/>,
<Flexbox align={ Flexbox.Alignment.Center } id='controls'>
<Button padding={ Button.Padding.None } onclick={ () => this.handlePlayPauseClick() }>
<Container padding={ 4 }>
<Icon color={ theme?.color?.secondary } glyph={ this.playing ? 'pause-fill' : 'play-fill' } size={ 24 }/>
</Container>
</Button>
<Spacer width={ 40 }/>
<Flexbox align={ Flexbox.Alignment.Center } grow>
<Typography>
{ String(currentTime.minute).padStart(2, '0') }:
{ String(currentTime.second).padStart(2, '0') }
</Typography>
<Spacer width={ 12 }/>
<Flexbox grow id='seek' onclick={ e => this.handleSeekClick(e) }>
<HTMLDivElement id='buffer' style={{ width: `${ (buffered.toSeconds() / duration.toSeconds()) * 100 }%` }}/>
<HTMLDivElement id='percent' style={{ width: `${ (currentTime.toSeconds() / duration.toSeconds()) * 100 }%` }}/>
</Flexbox>
<Spacer width={ 12 }/>
<Typography>
{ String(duration.minute).padStart(2, '0') }:
{ String(duration.second).padStart(2, '0') }
</Typography>
</Flexbox>
<Spacer width={ 40 }/>
<Button padding={ Button.Padding.None }>
<Container padding={ 8 }>
<Icon color={ theme?.color?.secondary } glyph='volume-down-fill' size={ 16 }/>
</Container>
</Button>
<Spacer width={ 12 }/>
<Button padding={ Button.Padding.None } onclick={ () => this.handlePicInPicClick() }>
<Container padding={ 8 }>
<Icon color={ theme?.color?.secondary } glyph='picture-in-picture-fill' size={ 16 }/>
</Container>
</Button>
<Spacer width={ 12 }/>
<Button padding={ Button.Padding.None } onclick={ () => this.handleFullscreenClick() }>
<Container padding={ 8 }>
<Icon color={ theme?.color?.secondary } glyph='fullscreen-fill' size={ 16 }/>
</Container>
</Button>
</Flexbox>
]
}
protected theme() {
return createStyleSheet(css => {
const theme = this.getContext(Theme)
css.selectHost(css => {
css.write(`
background: ${ theme?.color.background.replace(')', ' / 75%)') };
border: 1px solid ${ theme?.color.divider };
border-radius: 8px;
display: flex;
flex-direction: column;
flex-grow: 1;
max-height: 66vh;
min-height: 640px;
overflow: hidden;
position: relative;
`)
})
css.selectHostIs([ css.selectFullscreen() ], css => {
css.write(`
border: unset;
border-radius: unset;
max-height: unset;
min-height: unset;
`)
})
css.selectHostIs([ css.selectFocusWithin(), css.selectHover() ], css => {
css.write(`
box-shadow:
inset 0 0 80px ${ theme?.color?.secondary?.replace(')', ' / 9%)') },
0 8px 64px rgb(0 0 0 / 90%);
filter: drop-shadow(0 16px 32px ${ theme?.color?.secondary?.replace(')', ' / 9%)') });
`)
})
css.selectClass(HTMLIFrameElement, css => {
css.write(`
width: 100%;
height: 100%;
`)
})
css.selectId('action', css => {
css.write(`
height: calc(100% - 60px);
left: 0;
position: absolute;
top: 0;
width: 100%;
`)
css.selectDescendant(css => {
css.selectClass(Container, css => {
css.write(`
background: ${ theme?.color?.background };
border: 1px solid ${ theme?.color?.secondary };
border-radius: 8px;
opacity: 0.75;
padding: 12px 24px;
position: relative;
width: auto;
`)
css.selectHover(css => {
css.write(`
box-shadow:
inset 0 0 80px ${ theme?.color?.secondary?.replace(')', ' / 9%)') },
0 8px 64px rgb(0 0 0 / 90%);
filter: drop-shadow(0 16px 32px ${ theme?.color?.secondary?.replace(')', ' / 9%)') });
opacity: 1;
`)
})
})
css.selectClass(Icon, css => {
css.write(`
positon: relative;
z-index: 1;
`)
})
})
})
css.selectId('controls', css => {
css.write(`
padding: 12px;
`)
})
css.selectId('seek', css => {
css.write(`
background: ${ theme?.color?.middleground };
border: 1px solid black;
border-radius: 8px;
height: 10px;
overflow: hidden;
position: relative;
`)
css.selectAfter(css => {
css.write(`
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: 3;
`)
})
})
css.selectId('buffer', css => {
css.write(`
background-image: linear-gradient(
45deg,
${ theme?.color?.divider } 12.50%,
transparent 12.50%, transparent 50%,
${ theme?.color?.divider } 50%,
${ theme?.color?.divider } 62.50%,
transparent 62.50%,
transparent 100%
);
background-size: 4px 4px;
background-position: -1px;
border-radius: 8px;
bottom: 0;
left: 0;
position: absolute;
top: 0;
transition: width 100ms ease;
z-index: 1;
`)
})
css.selectId('percent', css => {
css.write(`
background: ${ theme?.color?.primary };
border-radius: 8px;
bottom: 0;
left: 0;
position: absolute;
top: 0;
transition: width 100ms ease;
z-index: 2;
`)
})
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment