Add autoplay error notification
This commit is contained in:
parent
0a40e7202a
commit
67d9177a91
@ -21,12 +21,10 @@ const initialize = (): InitializeAction => ({
|
|||||||
export const init = (): ThunkResult<Promise<void>> =>
|
export const init = (): ThunkResult<Promise<void>> =>
|
||||||
async (dispatch, getState) => {
|
async (dispatch, getState) => {
|
||||||
const socket = await dispatch(connect())
|
const socket = await dispatch(connect())
|
||||||
// const stream = await dispatch(getCameraStream())
|
|
||||||
|
|
||||||
dispatch(SocketActions.handshake({
|
dispatch(SocketActions.handshake({
|
||||||
socket,
|
socket,
|
||||||
roomName: callId,
|
roomName: callId,
|
||||||
// stream,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
dispatch(initialize())
|
dispatch(initialize())
|
||||||
@ -45,18 +43,3 @@ export const connect = () => (dispatch: Dispatch) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const getCameraStream = () => async (dispatch: Dispatch) => {
|
|
||||||
// try {
|
|
||||||
// const stream = await getUserMedia({
|
|
||||||
// video: { facingMode: 'user' },
|
|
||||||
// audio: true,
|
|
||||||
// })
|
|
||||||
// dispatch(StreamActions.addStream({ stream, userId: constants.ME }))
|
|
||||||
// return stream
|
|
||||||
// } catch (err) {
|
|
||||||
// dispatch(
|
|
||||||
// NotifyActions.alert('Could not get access to microphone & camera'))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@ -103,6 +103,18 @@ export function setMediaVisible(visible: boolean): MediaVisibleAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const play = makeAction('MEDIA_PLAY', async () => {
|
||||||
|
const promises = Array
|
||||||
|
.from(document.querySelectorAll('video'))
|
||||||
|
.filter(video => {
|
||||||
|
console.log('video', video.paused, video)
|
||||||
|
return video.paused
|
||||||
|
})
|
||||||
|
.map(video => video.play())
|
||||||
|
await Promise.all(promises)
|
||||||
|
})
|
||||||
|
|
||||||
export const getMediaStream = makeAction(
|
export const getMediaStream = makeAction(
|
||||||
MEDIA_STREAM,
|
MEDIA_STREAM,
|
||||||
async (constraints: GetMediaConstraints) => getUserMedia(constraints),
|
async (constraints: GetMediaConstraints) => getUserMedia(constraints),
|
||||||
@ -110,10 +122,12 @@ export const getMediaStream = makeAction(
|
|||||||
|
|
||||||
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
|
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
|
||||||
export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', MediaStream>
|
export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', MediaStream>
|
||||||
|
export type MediaPlayAction = AsyncAction<'MEDIA_PLAY', void>
|
||||||
|
|
||||||
export type MediaAction =
|
export type MediaAction =
|
||||||
MediaVideoConstraintAction |
|
MediaVideoConstraintAction |
|
||||||
MediaAudioConstraintAction |
|
MediaAudioConstraintAction |
|
||||||
MediaEnumerateAction |
|
MediaEnumerateAction |
|
||||||
MediaStreamAction |
|
MediaStreamAction |
|
||||||
MediaVisibleAction
|
MediaVisibleAction |
|
||||||
|
MediaPlayAction
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import * as PeerActions from './PeerActions'
|
|||||||
import Peer from 'simple-peer'
|
import Peer from 'simple-peer'
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { createStore, Store, GetState } from '../store'
|
import { createStore, Store, GetState } from '../store'
|
||||||
import { play } from '../window'
|
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
import { ClientSocket } from '../socket'
|
import { ClientSocket } from '../socket'
|
||||||
|
|
||||||
@ -33,8 +32,7 @@ describe('PeerActions', () => {
|
|||||||
user = { id: 'user2' }
|
user = { id: 'user2' }
|
||||||
socket = createSocket()
|
socket = createSocket()
|
||||||
instances = (Peer as any).instances = [];
|
instances = (Peer as any).instances = [];
|
||||||
(Peer as unknown as jest.Mock).mockClear();
|
(Peer as unknown as jest.Mock).mockClear()
|
||||||
(play as jest.Mock).mockClear()
|
|
||||||
stream = { stream: true } as unknown as MediaStream
|
stream = { stream: true } as unknown as MediaStream
|
||||||
PeerMock = Peer as unknown as jest.Mock<Peer.Instance>
|
PeerMock = Peer as unknown as jest.Mock<Peer.Instance>
|
||||||
})
|
})
|
||||||
@ -87,8 +85,8 @@ describe('PeerActions', () => {
|
|||||||
describe('connect', () => {
|
describe('connect', () => {
|
||||||
beforeEach(() => peer.emit('connect'))
|
beforeEach(() => peer.emit('connect'))
|
||||||
|
|
||||||
it('dispatches "play" action', () => {
|
it('dispatches peer connection established message', () => {
|
||||||
expect((play as jest.Mock).mock.calls.length).toBe(1)
|
// TODO
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import * as constants from '../constants'
|
|||||||
import Peer, { SignalData } from 'simple-peer'
|
import Peer, { SignalData } from 'simple-peer'
|
||||||
import forEach from 'lodash/forEach'
|
import forEach from 'lodash/forEach'
|
||||||
import _debug from 'debug'
|
import _debug from 'debug'
|
||||||
import { play, iceServers } from '../window'
|
import { iceServers } from '../window'
|
||||||
import { Dispatch, GetState } from '../store'
|
import { Dispatch, GetState } from '../store'
|
||||||
import { ClientSocket } from '../socket'
|
import { ClientSocket } from '../socket'
|
||||||
|
|
||||||
@ -50,10 +50,19 @@ class PeerHandler {
|
|||||||
socket.emit('signal', payload)
|
socket.emit('signal', payload)
|
||||||
}
|
}
|
||||||
handleConnect = () => {
|
handleConnect = () => {
|
||||||
const { dispatch, user } = this
|
const { dispatch, user, getState } = this
|
||||||
debug('peer: %s, connect', user.id)
|
debug('peer: %s, connect', user.id)
|
||||||
dispatch(NotifyActions.warning('Peer connection established'))
|
dispatch(NotifyActions.warning('Peer connection established'))
|
||||||
play()
|
|
||||||
|
const state = getState()
|
||||||
|
const peer = state.peers[user.id]
|
||||||
|
const localStream = state.streams[constants.ME]
|
||||||
|
if (localStream && localStream.stream) {
|
||||||
|
// If the local user pressed join call before this peer has joined the
|
||||||
|
// call, now is the time to share local media stream with the peer since
|
||||||
|
// we no longer automatically send the stream to the peer.
|
||||||
|
peer.addStream(localStream.stream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleStream = (stream: MediaStream) => {
|
handleStream = (stream: MediaStream) => {
|
||||||
const { user, dispatch } = this
|
const { user, dispatch } = this
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export interface AppProps {
|
|||||||
messages: Message[]
|
messages: Message[]
|
||||||
messagesCount: number
|
messagesCount: number
|
||||||
peers: Record<string, Peer.Instance>
|
peers: Record<string, Peer.Instance>
|
||||||
|
play: () => void
|
||||||
sendMessage: (message: TextMessage) => void
|
sendMessage: (message: TextMessage) => void
|
||||||
streams: Record<string, AddStreamPayload>
|
streams: Record<string, AddStreamPayload>
|
||||||
onSendFile: (file: File) => void
|
onSendFile: (file: File) => void
|
||||||
@ -66,6 +67,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
messages,
|
messages,
|
||||||
messagesCount,
|
messagesCount,
|
||||||
onSendFile,
|
onSendFile,
|
||||||
|
play,
|
||||||
peers,
|
peers,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
toggleActive,
|
toggleActive,
|
||||||
@ -97,6 +99,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
videos={videos}
|
videos={videos}
|
||||||
active={active === constants.ME}
|
active={active === constants.ME}
|
||||||
onClick={toggleActive}
|
onClick={toggleActive}
|
||||||
|
play={play}
|
||||||
stream={streams[constants.ME]}
|
stream={streams[constants.ME]}
|
||||||
userId={constants.ME}
|
userId={constants.ME}
|
||||||
muted
|
muted
|
||||||
@ -108,6 +111,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
active={userId === active}
|
active={userId === active}
|
||||||
key={userId}
|
key={userId}
|
||||||
onClick={toggleActive}
|
onClick={toggleActive}
|
||||||
|
play={play}
|
||||||
stream={streams[userId]}
|
stream={streams[userId]}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
videos={videos}
|
videos={videos}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ describe('Media', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
it('tries to retrieve audio/video media stream', async () => {
|
it('tries to retrieve audio/video media stream', async () => {
|
||||||
const node = await render()
|
const node = (await render()).querySelector('.media')!
|
||||||
expect(node.tagName).toBe('FORM')
|
expect(node.tagName).toBe('FORM')
|
||||||
TestUtils.Simulate.submit(node)
|
TestUtils.Simulate.submit(node)
|
||||||
expect(promise).toBeDefined()
|
expect(promise).toBeDefined()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { AudioConstraint, MediaDevice, setAudioConstraint, setVideoConstraint, VideoConstraint, getMediaStream, enumerateDevices } from '../actions/MediaActions'
|
import { AudioConstraint, MediaDevice, setAudioConstraint, setVideoConstraint, VideoConstraint, getMediaStream, enumerateDevices, play } from '../actions/MediaActions'
|
||||||
import { MediaState } from '../reducers/media'
|
import { MediaState } from '../reducers/media'
|
||||||
import { State } from '../store'
|
import { State } from '../store'
|
||||||
|
|
||||||
@ -9,6 +9,7 @@ export type MediaProps = MediaState & {
|
|||||||
onSetVideoConstraint: typeof setVideoConstraint
|
onSetVideoConstraint: typeof setVideoConstraint
|
||||||
onSetAudioConstraint: typeof setAudioConstraint
|
onSetAudioConstraint: typeof setAudioConstraint
|
||||||
getMediaStream: typeof getMediaStream
|
getMediaStream: typeof getMediaStream
|
||||||
|
play: typeof play
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: State) {
|
function mapStateToProps(state: State) {
|
||||||
@ -22,11 +23,12 @@ const mapDispatchToProps = {
|
|||||||
onSetVideoConstraint: setVideoConstraint,
|
onSetVideoConstraint: setVideoConstraint,
|
||||||
onSetAudioConstraint: setAudioConstraint,
|
onSetAudioConstraint: setAudioConstraint,
|
||||||
getMediaStream,
|
getMediaStream,
|
||||||
|
play,
|
||||||
}
|
}
|
||||||
|
|
||||||
const c = connect(mapStateToProps, mapDispatchToProps)
|
const c = connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
|
||||||
export const Media = c(React.memo(function Media(props: MediaProps) {
|
export const MediaForm = React.memo(function MediaForm(props: MediaProps) {
|
||||||
if (!props.visible) {
|
if (!props.visible) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -88,6 +90,34 @@ export const Media = c(React.memo(function Media(props: MediaProps) {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface AutoplayProps {
|
||||||
|
play: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AutoplayMessage = React.memo(
|
||||||
|
function Autoplay(props: AutoplayProps) {
|
||||||
|
return (
|
||||||
|
<div className='autoplay'>
|
||||||
|
The browser has blocked video autoplay on this page.
|
||||||
|
To continue with your call, please press the play button:
|
||||||
|
|
||||||
|
<button className='button' onClick={props.play}>
|
||||||
|
Play
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Media = c(React.memo(function Media(props: MediaProps) {
|
||||||
|
return (
|
||||||
|
<div className='media-container'>
|
||||||
|
{props.autoplayError && <AutoplayMessage play={props.play} />}
|
||||||
|
<MediaForm {...props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
interface OptionsProps {
|
interface OptionsProps {
|
||||||
|
|||||||
@ -12,6 +12,8 @@ describe('components/Video', () => {
|
|||||||
stream: null | AddStreamPayload
|
stream: null | AddStreamPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const play = jest.fn()
|
||||||
|
|
||||||
class VideoWrapper extends React.PureComponent<VideoProps, VideoState> {
|
class VideoWrapper extends React.PureComponent<VideoProps, VideoState> {
|
||||||
ref = React.createRef<Video>()
|
ref = React.createRef<Video>()
|
||||||
|
|
||||||
@ -26,6 +28,7 @@ describe('components/Video', () => {
|
|||||||
active={this.props.active}
|
active={this.props.active}
|
||||||
stream={this.state.stream || this.props.stream}
|
stream={this.state.stream || this.props.stream}
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
|
play={this.props.play}
|
||||||
userId="test"
|
userId="test"
|
||||||
muted={this.props.muted}
|
muted={this.props.muted}
|
||||||
mirrored={this.props.mirrored}
|
mirrored={this.props.mirrored}
|
||||||
@ -65,6 +68,7 @@ describe('components/Video', () => {
|
|||||||
active={flags.active}
|
active={flags.active}
|
||||||
stream={{ stream: mediaStream, url, userId: 'test' }}
|
stream={{ stream: mediaStream, url, userId: 'test' }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
play={play}
|
||||||
userId="test"
|
userId="test"
|
||||||
muted={flags.muted}
|
muted={flags.muted}
|
||||||
mirrored={flags.mirrored}
|
mirrored={flags.mirrored}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export interface VideoProps {
|
|||||||
userId: string
|
userId: string
|
||||||
muted: boolean
|
muted: boolean
|
||||||
mirrored: boolean
|
mirrored: boolean
|
||||||
|
play: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Video extends React.PureComponent<VideoProps> {
|
export default class Video extends React.PureComponent<VideoProps> {
|
||||||
@ -22,13 +23,9 @@ export default class Video extends React.PureComponent<VideoProps> {
|
|||||||
}
|
}
|
||||||
handleClick: ReactEventHandler<HTMLVideoElement> = e => {
|
handleClick: ReactEventHandler<HTMLVideoElement> = e => {
|
||||||
const { onClick, userId } = this.props
|
const { onClick, userId } = this.props
|
||||||
this.play(e)
|
this.props.play()
|
||||||
onClick(userId)
|
onClick(userId)
|
||||||
}
|
}
|
||||||
play: ReactEventHandler<HTMLVideoElement> = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
(e.target as HTMLVideoElement).play()
|
|
||||||
}
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.componentDidUpdate()
|
this.componentDidUpdate()
|
||||||
}
|
}
|
||||||
@ -55,7 +52,10 @@ export default class Video extends React.PureComponent<VideoProps> {
|
|||||||
id={`video-${socket.id}`}
|
id={`video-${socket.id}`}
|
||||||
autoPlay
|
autoPlay
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onLoadedMetadata={this.play}
|
onLoadedMetadata={() => {
|
||||||
|
console.log('onLoadedMetadata')
|
||||||
|
this.props.play()
|
||||||
|
}}
|
||||||
playsInline
|
playsInline
|
||||||
ref={this.videoRef}
|
ref={this.videoRef}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export const MEDIA_ENUMERATE = 'MEDIA_ENUMERATE'
|
|||||||
export const MEDIA_STREAM = 'MEDIA_STREAM'
|
export const MEDIA_STREAM = 'MEDIA_STREAM'
|
||||||
export const MEDIA_VIDEO_CONSTRAINT_SET = 'MEDIA_VIDEO_CONSTRAINT_SET'
|
export const MEDIA_VIDEO_CONSTRAINT_SET = 'MEDIA_VIDEO_CONSTRAINT_SET'
|
||||||
export const MEDIA_AUDIO_CONSTRAINT_SET = 'MEDIA_AUDIO_CONSTRAINT_SET'
|
export const MEDIA_AUDIO_CONSTRAINT_SET = 'MEDIA_AUDIO_CONSTRAINT_SET'
|
||||||
|
export const MEDIA_PLAY = 'MEDIA_PLAY'
|
||||||
export const MEDIA_VISIBLE_SET = 'MEDIA_VISIBLE_SET'
|
export const MEDIA_VISIBLE_SET = 'MEDIA_VISIBLE_SET'
|
||||||
|
|
||||||
export const PEER_ADD = 'PEER_ADD'
|
export const PEER_ADD = 'PEER_ADD'
|
||||||
|
|||||||
@ -112,7 +112,8 @@ describe('App', () => {
|
|||||||
dispatchSpy.mockReset()
|
dispatchSpy.mockReset()
|
||||||
const video = node.querySelector('video')!
|
const video = node.querySelector('video')!
|
||||||
TestUtils.Simulate.click(video)
|
TestUtils.Simulate.click(video)
|
||||||
expect(dispatchSpy.mock.calls).toEqual([[{
|
expect(dispatchSpy.mock.calls[0][0].type).toBe(constants.MEDIA_PLAY)
|
||||||
|
expect(dispatchSpy.mock.calls.slice(1)).toEqual([[{
|
||||||
type: constants.ACTIVE_TOGGLE,
|
type: constants.ACTIVE_TOGGLE,
|
||||||
payload: { userId: constants.ME },
|
payload: { userId: constants.ME },
|
||||||
}]])
|
}]])
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import * as CallActions from '../actions/CallActions'
|
|
||||||
import * as NotifyActions from '../actions/NotifyActions'
|
|
||||||
import * as PeerActions from '../actions/PeerActions'
|
|
||||||
import * as StreamActions from '../actions/StreamActions'
|
|
||||||
import App from '../components/App'
|
|
||||||
import { bindActionCreators, Dispatch } from 'redux'
|
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
import { init } from '../actions/CallActions'
|
||||||
|
import { play } from '../actions/MediaActions'
|
||||||
|
import { dismissAlert } from '../actions/NotifyActions'
|
||||||
|
import { sendFile, sendMessage } from '../actions/PeerActions'
|
||||||
|
import { toggleActive } from '../actions/StreamActions'
|
||||||
|
import App from '../components/App'
|
||||||
import { State } from '../store'
|
import { State } from '../store'
|
||||||
|
|
||||||
function mapStateToProps (state: State) {
|
function mapStateToProps (state: State) {
|
||||||
@ -19,14 +19,13 @@ function mapStateToProps (state: State) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps (dispatch: Dispatch) {
|
const mapDispatchToProps = {
|
||||||
return {
|
toggleActive,
|
||||||
toggleActive: bindActionCreators(StreamActions.toggleActive, dispatch),
|
sendMessage,
|
||||||
sendMessage: bindActionCreators(PeerActions.sendMessage, dispatch),
|
dismissAlert: dismissAlert,
|
||||||
dismissAlert: bindActionCreators(NotifyActions.dismissAlert, dispatch),
|
init,
|
||||||
init: bindActionCreators(CallActions.init, dispatch),
|
onSendFile: sendFile,
|
||||||
onSendFile: bindActionCreators(PeerActions.sendFile, dispatch),
|
play,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(App)
|
export default connect(mapStateToProps, mapDispatchToProps)(App)
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { play } from './window'
|
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
@ -14,4 +13,3 @@ const component = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
ReactDOM.render(component, document.getElementById('container'))
|
ReactDOM.render(component, document.getElementById('container'))
|
||||||
play()
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { MediaDevice, AudioConstraint, VideoConstraint, MediaAction, MediaEnumerateAction, MediaStreamAction } from '../actions/MediaActions'
|
import { MediaDevice, AudioConstraint, VideoConstraint, MediaAction, MediaEnumerateAction, MediaStreamAction, MediaPlayAction } from '../actions/MediaActions'
|
||||||
import { MEDIA_ENUMERATE, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_VISIBLE_SET, MEDIA_STREAM } from '../constants'
|
import { MEDIA_ENUMERATE, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_VISIBLE_SET, MEDIA_STREAM, MEDIA_PLAY } from '../constants'
|
||||||
|
|
||||||
export interface MediaState {
|
export interface MediaState {
|
||||||
devices: MediaDevice[]
|
devices: MediaDevice[]
|
||||||
@ -7,6 +7,7 @@ export interface MediaState {
|
|||||||
audio: AudioConstraint
|
audio: AudioConstraint
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: string
|
error: string
|
||||||
|
autoplayError: boolean
|
||||||
visible: boolean
|
visible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ const defaultState: MediaState = {
|
|||||||
audio: true,
|
audio: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: '',
|
error: '',
|
||||||
|
autoplayError: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +67,27 @@ export function handleMediaStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handlePlay(
|
||||||
|
state: MediaState,
|
||||||
|
action: MediaPlayAction,
|
||||||
|
): MediaState {
|
||||||
|
switch (action.status) {
|
||||||
|
case 'pending':
|
||||||
|
case 'resolved':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
autoplayError: false,
|
||||||
|
}
|
||||||
|
case 'rejected':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
autoplayError: true,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function media(
|
export default function media(
|
||||||
state = defaultState,
|
state = defaultState,
|
||||||
action: MediaAction,
|
action: MediaAction,
|
||||||
@ -89,6 +112,8 @@ export default function media(
|
|||||||
}
|
}
|
||||||
case MEDIA_STREAM:
|
case MEDIA_STREAM:
|
||||||
return handleMediaStream(state, action)
|
return handleMediaStream(state, action)
|
||||||
|
case MEDIA_PLAY:
|
||||||
|
return handlePlay(state, action)
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +1,7 @@
|
|||||||
import { createObjectURL, play, revokeObjectURL, valueOf } from './window'
|
import { createObjectURL, revokeObjectURL, valueOf } from './window'
|
||||||
|
|
||||||
describe('window', () => {
|
describe('window', () => {
|
||||||
|
|
||||||
describe('play', () => {
|
|
||||||
|
|
||||||
let v1: HTMLVideoElement & { play: jest.Mock }
|
|
||||||
let v2: HTMLVideoElement & { play: jest.Mock }
|
|
||||||
beforeEach(() => {
|
|
||||||
v1 = window.document.createElement('video') as any
|
|
||||||
v2 = window.document.createElement('video') as any
|
|
||||||
window.document.body.appendChild(v1)
|
|
||||||
window.document.body.appendChild(v2)
|
|
||||||
v1.play = jest.fn()
|
|
||||||
v2.play = jest.fn()
|
|
||||||
})
|
|
||||||
afterEach(() => {
|
|
||||||
window.document.body.removeChild(v1)
|
|
||||||
window.document.body.removeChild(v2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('gets all videos and plays them', () => {
|
|
||||||
play()
|
|
||||||
expect(v1.play.mock.calls.length).toBe(1)
|
|
||||||
expect(v2.play.mock.calls.length).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not fail on error', () => {
|
|
||||||
v1.play.mockImplementation(() => { throw new Error('test') })
|
|
||||||
play()
|
|
||||||
expect(v1.play.mock.calls.length).toBe(1)
|
|
||||||
expect(v2.play.mock.calls.length).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('navigator', () => {
|
describe('navigator', () => {
|
||||||
|
|
||||||
it('exposes window.navigator', () => {
|
it('exposes window.navigator', () => {
|
||||||
|
|||||||
@ -1,23 +1,7 @@
|
|||||||
import _debug from 'debug'
|
|
||||||
|
|
||||||
const debug = _debug('peercalls')
|
|
||||||
|
|
||||||
export const createObjectURL = (object: unknown) =>
|
export const createObjectURL = (object: unknown) =>
|
||||||
window.URL.createObjectURL(object)
|
window.URL.createObjectURL(object)
|
||||||
export const revokeObjectURL = (url: string) => window.URL.revokeObjectURL(url)
|
export const revokeObjectURL = (url: string) => window.URL.revokeObjectURL(url)
|
||||||
|
|
||||||
export function play () {
|
|
||||||
const videos = window.document.querySelectorAll('video')
|
|
||||||
Array.prototype.forEach.call(videos, (video, index) => {
|
|
||||||
debug('playing video: %s', index)
|
|
||||||
try {
|
|
||||||
video.play()
|
|
||||||
} catch (e) {
|
|
||||||
debug('error playing video: %s', e.name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const valueOf = (id: string) => {
|
export const valueOf = (id: string) => {
|
||||||
const el = window.document.getElementById(id) as HTMLInputElement
|
const el = window.document.getElementById(id) as HTMLInputElement
|
||||||
return el && el.value
|
return el && el.value
|
||||||
|
|||||||
@ -1,4 +1,21 @@
|
|||||||
form.media {
|
.media-container .autoplay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
color: $color-warning;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.5rem;
|
||||||
|
@include button($color-primary, $color-warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-container form.media {
|
||||||
max-width: 440px;
|
max-width: 440px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -29,13 +46,5 @@ form.media {
|
|||||||
button {
|
button {
|
||||||
@include button($color-primary, $color-warning);
|
@include button($color-primary, $color-warning);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@include button($color-primary, darken($color-warning, 5%));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
@include button($color-primary, darken($color-warning, 10%));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ body.call {
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin button($color-fg, $color-bg) {
|
@mixin button-style($color-fg, $color-bg) {
|
||||||
background-color: $color-bg;
|
background-color: $color-bg;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 2px solid darken($color-bg, 10%);
|
border-bottom: 2px solid darken($color-bg, 10%);
|
||||||
@ -60,6 +60,22 @@ body.call {
|
|||||||
text-shadow: 0 0 0.35rem rgba(0, 0, 0, 0.6);
|
text-shadow: 0 0 0.35rem rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin button($color-fg, $color-bg) {
|
||||||
|
@include button-style($color-fg, $color-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
outline: none;
|
||||||
|
@include button-style($color-fg, darken($color-bg, 5%));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
outline: none;
|
||||||
|
transform: translate(0px, 1px);
|
||||||
|
@include button-style($color-fg, darken($color-bg, 10%));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#form {
|
#form {
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -86,24 +102,6 @@ body.call {
|
|||||||
padding: 1rem 1rem;
|
padding: 1rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:hover {
|
|
||||||
@include button($color-primary, darken($color-warning, 5%));
|
|
||||||
}
|
|
||||||
|
|
||||||
input:active {
|
|
||||||
transform: translate(0px, 1px);
|
|
||||||
@include button($color-primary, darken($color-warning, 10%));
|
|
||||||
}
|
|
||||||
|
|
||||||
input:active,
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="submit"] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-input-placeholder {
|
::-webkit-input-placeholder {
|
||||||
color: $color-fg;
|
color: $color-fg;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user