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>> =>
|
||||
async (dispatch, getState) => {
|
||||
const socket = await dispatch(connect())
|
||||
// const stream = await dispatch(getCameraStream())
|
||||
|
||||
dispatch(SocketActions.handshake({
|
||||
socket,
|
||||
roomName: callId,
|
||||
// stream,
|
||||
}))
|
||||
|
||||
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(
|
||||
MEDIA_STREAM,
|
||||
async (constraints: GetMediaConstraints) => getUserMedia(constraints),
|
||||
@ -110,10 +122,12 @@ export const getMediaStream = makeAction(
|
||||
|
||||
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
|
||||
export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', MediaStream>
|
||||
export type MediaPlayAction = AsyncAction<'MEDIA_PLAY', void>
|
||||
|
||||
export type MediaAction =
|
||||
MediaVideoConstraintAction |
|
||||
MediaAudioConstraintAction |
|
||||
MediaEnumerateAction |
|
||||
MediaStreamAction |
|
||||
MediaVisibleAction
|
||||
MediaVisibleAction |
|
||||
MediaPlayAction
|
||||
|
||||
@ -5,7 +5,6 @@ import * as PeerActions from './PeerActions'
|
||||
import Peer from 'simple-peer'
|
||||
import { EventEmitter } from 'events'
|
||||
import { createStore, Store, GetState } from '../store'
|
||||
import { play } from '../window'
|
||||
import { Dispatch } from 'redux'
|
||||
import { ClientSocket } from '../socket'
|
||||
|
||||
@ -33,8 +32,7 @@ describe('PeerActions', () => {
|
||||
user = { id: 'user2' }
|
||||
socket = createSocket()
|
||||
instances = (Peer as any).instances = [];
|
||||
(Peer as unknown as jest.Mock).mockClear();
|
||||
(play as jest.Mock).mockClear()
|
||||
(Peer as unknown as jest.Mock).mockClear()
|
||||
stream = { stream: true } as unknown as MediaStream
|
||||
PeerMock = Peer as unknown as jest.Mock<Peer.Instance>
|
||||
})
|
||||
@ -87,8 +85,8 @@ describe('PeerActions', () => {
|
||||
describe('connect', () => {
|
||||
beforeEach(() => peer.emit('connect'))
|
||||
|
||||
it('dispatches "play" action', () => {
|
||||
expect((play as jest.Mock).mock.calls.length).toBe(1)
|
||||
it('dispatches peer connection established message', () => {
|
||||
// TODO
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import * as constants from '../constants'
|
||||
import Peer, { SignalData } from 'simple-peer'
|
||||
import forEach from 'lodash/forEach'
|
||||
import _debug from 'debug'
|
||||
import { play, iceServers } from '../window'
|
||||
import { iceServers } from '../window'
|
||||
import { Dispatch, GetState } from '../store'
|
||||
import { ClientSocket } from '../socket'
|
||||
|
||||
@ -50,10 +50,19 @@ class PeerHandler {
|
||||
socket.emit('signal', payload)
|
||||
}
|
||||
handleConnect = () => {
|
||||
const { dispatch, user } = this
|
||||
const { dispatch, user, getState } = this
|
||||
debug('peer: %s, connect', user.id)
|
||||
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) => {
|
||||
const { user, dispatch } = this
|
||||
|
||||
@ -22,6 +22,7 @@ export interface AppProps {
|
||||
messages: Message[]
|
||||
messagesCount: number
|
||||
peers: Record<string, Peer.Instance>
|
||||
play: () => void
|
||||
sendMessage: (message: TextMessage) => void
|
||||
streams: Record<string, AddStreamPayload>
|
||||
onSendFile: (file: File) => void
|
||||
@ -66,6 +67,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
messages,
|
||||
messagesCount,
|
||||
onSendFile,
|
||||
play,
|
||||
peers,
|
||||
sendMessage,
|
||||
toggleActive,
|
||||
@ -97,6 +99,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
videos={videos}
|
||||
active={active === constants.ME}
|
||||
onClick={toggleActive}
|
||||
play={play}
|
||||
stream={streams[constants.ME]}
|
||||
userId={constants.ME}
|
||||
muted
|
||||
@ -108,6 +111,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
active={userId === active}
|
||||
key={userId}
|
||||
onClick={toggleActive}
|
||||
play={play}
|
||||
stream={streams[userId]}
|
||||
userId={userId}
|
||||
videos={videos}
|
||||
|
||||
@ -53,7 +53,7 @@ describe('Media', () => {
|
||||
}
|
||||
})
|
||||
it('tries to retrieve audio/video media stream', async () => {
|
||||
const node = await render()
|
||||
const node = (await render()).querySelector('.media')!
|
||||
expect(node.tagName).toBe('FORM')
|
||||
TestUtils.Simulate.submit(node)
|
||||
expect(promise).toBeDefined()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
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 { State } from '../store'
|
||||
|
||||
@ -9,6 +9,7 @@ export type MediaProps = MediaState & {
|
||||
onSetVideoConstraint: typeof setVideoConstraint
|
||||
onSetAudioConstraint: typeof setAudioConstraint
|
||||
getMediaStream: typeof getMediaStream
|
||||
play: typeof play
|
||||
}
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
@ -22,11 +23,12 @@ const mapDispatchToProps = {
|
||||
onSetVideoConstraint: setVideoConstraint,
|
||||
onSetAudioConstraint: setAudioConstraint,
|
||||
getMediaStream,
|
||||
play,
|
||||
}
|
||||
|
||||
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) {
|
||||
return null
|
||||
}
|
||||
@ -88,6 +90,34 @@ export const Media = c(React.memo(function Media(props: MediaProps) {
|
||||
</button>
|
||||
</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 {
|
||||
|
||||
@ -12,6 +12,8 @@ describe('components/Video', () => {
|
||||
stream: null | AddStreamPayload
|
||||
}
|
||||
|
||||
const play = jest.fn()
|
||||
|
||||
class VideoWrapper extends React.PureComponent<VideoProps, VideoState> {
|
||||
ref = React.createRef<Video>()
|
||||
|
||||
@ -26,6 +28,7 @@ describe('components/Video', () => {
|
||||
active={this.props.active}
|
||||
stream={this.state.stream || this.props.stream}
|
||||
onClick={this.props.onClick}
|
||||
play={this.props.play}
|
||||
userId="test"
|
||||
muted={this.props.muted}
|
||||
mirrored={this.props.mirrored}
|
||||
@ -65,6 +68,7 @@ describe('components/Video', () => {
|
||||
active={flags.active}
|
||||
stream={{ stream: mediaStream, url, userId: 'test' }}
|
||||
onClick={onClick}
|
||||
play={play}
|
||||
userId="test"
|
||||
muted={flags.muted}
|
||||
mirrored={flags.mirrored}
|
||||
|
||||
@ -11,6 +11,7 @@ export interface VideoProps {
|
||||
userId: string
|
||||
muted: boolean
|
||||
mirrored: boolean
|
||||
play: () => void
|
||||
}
|
||||
|
||||
export default class Video extends React.PureComponent<VideoProps> {
|
||||
@ -22,13 +23,9 @@ export default class Video extends React.PureComponent<VideoProps> {
|
||||
}
|
||||
handleClick: ReactEventHandler<HTMLVideoElement> = e => {
|
||||
const { onClick, userId } = this.props
|
||||
this.play(e)
|
||||
this.props.play()
|
||||
onClick(userId)
|
||||
}
|
||||
play: ReactEventHandler<HTMLVideoElement> = e => {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLVideoElement).play()
|
||||
}
|
||||
componentDidMount () {
|
||||
this.componentDidUpdate()
|
||||
}
|
||||
@ -55,7 +52,10 @@ export default class Video extends React.PureComponent<VideoProps> {
|
||||
id={`video-${socket.id}`}
|
||||
autoPlay
|
||||
onClick={this.handleClick}
|
||||
onLoadedMetadata={this.play}
|
||||
onLoadedMetadata={() => {
|
||||
console.log('onLoadedMetadata')
|
||||
this.props.play()
|
||||
}}
|
||||
playsInline
|
||||
ref={this.videoRef}
|
||||
muted={muted}
|
||||
|
||||
@ -19,6 +19,7 @@ export const MEDIA_ENUMERATE = 'MEDIA_ENUMERATE'
|
||||
export const MEDIA_STREAM = 'MEDIA_STREAM'
|
||||
export const MEDIA_VIDEO_CONSTRAINT_SET = 'MEDIA_VIDEO_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 PEER_ADD = 'PEER_ADD'
|
||||
|
||||
@ -112,7 +112,8 @@ describe('App', () => {
|
||||
dispatchSpy.mockReset()
|
||||
const video = node.querySelector('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,
|
||||
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 { 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'
|
||||
|
||||
function mapStateToProps (state: State) {
|
||||
@ -19,14 +19,13 @@ function mapStateToProps (state: State) {
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch: Dispatch) {
|
||||
return {
|
||||
toggleActive: bindActionCreators(StreamActions.toggleActive, dispatch),
|
||||
sendMessage: bindActionCreators(PeerActions.sendMessage, dispatch),
|
||||
dismissAlert: bindActionCreators(NotifyActions.dismissAlert, dispatch),
|
||||
init: bindActionCreators(CallActions.init, dispatch),
|
||||
onSendFile: bindActionCreators(PeerActions.sendFile, dispatch),
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
toggleActive,
|
||||
sendMessage,
|
||||
dismissAlert: dismissAlert,
|
||||
init,
|
||||
onSendFile: sendFile,
|
||||
play,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App)
|
||||
|
||||
@ -5,7 +5,6 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import store from './store'
|
||||
import { Provider } from 'react-redux'
|
||||
import { play } from './window'
|
||||
|
||||
const component = (
|
||||
<Provider store={store}>
|
||||
@ -14,4 +13,3 @@ const component = (
|
||||
)
|
||||
|
||||
ReactDOM.render(component, document.getElementById('container'))
|
||||
play()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { MediaDevice, AudioConstraint, VideoConstraint, MediaAction, MediaEnumerateAction, MediaStreamAction } from '../actions/MediaActions'
|
||||
import { MEDIA_ENUMERATE, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_VISIBLE_SET, MEDIA_STREAM } from '../constants'
|
||||
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, MEDIA_PLAY } from '../constants'
|
||||
|
||||
export interface MediaState {
|
||||
devices: MediaDevice[]
|
||||
@ -7,6 +7,7 @@ export interface MediaState {
|
||||
audio: AudioConstraint
|
||||
loading: boolean
|
||||
error: string
|
||||
autoplayError: boolean
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
@ -16,6 +17,7 @@ const defaultState: MediaState = {
|
||||
audio: true,
|
||||
loading: false,
|
||||
error: '',
|
||||
autoplayError: false,
|
||||
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(
|
||||
state = defaultState,
|
||||
action: MediaAction,
|
||||
@ -89,6 +112,8 @@ export default function media(
|
||||
}
|
||||
case MEDIA_STREAM:
|
||||
return handleMediaStream(state, action)
|
||||
case MEDIA_PLAY:
|
||||
return handlePlay(state, action)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
||||
@ -1,39 +1,7 @@
|
||||
import { createObjectURL, play, revokeObjectURL, valueOf } from './window'
|
||||
import { createObjectURL, revokeObjectURL, valueOf } from './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', () => {
|
||||
|
||||
it('exposes window.navigator', () => {
|
||||
|
||||
@ -1,23 +1,7 @@
|
||||
import _debug from 'debug'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
|
||||
export const createObjectURL = (object: unknown) =>
|
||||
window.URL.createObjectURL(object)
|
||||
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) => {
|
||||
const el = window.document.getElementById(id) as HTMLInputElement
|
||||
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;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
@ -29,13 +46,5 @@ form.media {
|
||||
button {
|
||||
@include button($color-primary, $color-warning);
|
||||
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;
|
||||
}
|
||||
|
||||
@mixin button($color-fg, $color-bg) {
|
||||
@mixin button-style($color-fg, $color-bg) {
|
||||
background-color: $color-bg;
|
||||
border: none;
|
||||
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);
|
||||
}
|
||||
|
||||
@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 {
|
||||
padding-top: 50px;
|
||||
text-align: center;
|
||||
@ -86,24 +102,6 @@ body.call {
|
||||
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 {
|
||||
color: $color-fg;
|
||||
text-align: center;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user