From 67d9177a91e2f3030578831797a782d21fe8d30d Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Sun, 17 Nov 2019 10:45:46 -0300 Subject: [PATCH] Add autoplay error notification --- src/client/actions/CallActions.ts | 17 ------------ src/client/actions/MediaActions.ts | 16 +++++++++++- src/client/actions/PeerActions.test.ts | 8 +++--- src/client/actions/PeerActions.ts | 15 ++++++++--- src/client/components/App.tsx | 4 +++ src/client/components/Media.test.tsx | 2 +- src/client/components/Media.tsx | 34 ++++++++++++++++++++++-- src/client/components/Video.test.tsx | 4 +++ src/client/components/Video.tsx | 12 ++++----- src/client/constants.ts | 1 + src/client/containers/App.test.tsx | 3 ++- src/client/containers/App.tsx | 27 ++++++++++--------- src/client/index.tsx | 2 -- src/client/reducers/media.ts | 29 +++++++++++++++++++-- src/client/window.test.ts | 34 +----------------------- src/client/window.ts | 16 ------------ src/scss/_media.scss | 27 ++++++++++++------- src/scss/style.scss | 36 ++++++++++++-------------- 18 files changed, 156 insertions(+), 131 deletions(-) diff --git a/src/client/actions/CallActions.ts b/src/client/actions/CallActions.ts index 84fc732..3c7aa14 100644 --- a/src/client/actions/CallActions.ts +++ b/src/client/actions/CallActions.ts @@ -21,12 +21,10 @@ const initialize = (): InitializeAction => ({ export const init = (): ThunkResult> => 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 -// } -// } diff --git a/src/client/actions/MediaActions.ts b/src/client/actions/MediaActions.ts index 44d8230..cbf598e 100644 --- a/src/client/actions/MediaActions.ts +++ b/src/client/actions/MediaActions.ts @@ -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 diff --git a/src/client/actions/PeerActions.test.ts b/src/client/actions/PeerActions.test.ts index 91441d7..f346ccc 100644 --- a/src/client/actions/PeerActions.test.ts +++ b/src/client/actions/PeerActions.test.ts @@ -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 }) @@ -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 }) }) diff --git a/src/client/actions/PeerActions.ts b/src/client/actions/PeerActions.ts index 59ff633..c0ff94d 100644 --- a/src/client/actions/PeerActions.ts +++ b/src/client/actions/PeerActions.ts @@ -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 diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 97ce7b3..2d735b4 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -22,6 +22,7 @@ export interface AppProps { messages: Message[] messagesCount: number peers: Record + play: () => void sendMessage: (message: TextMessage) => void streams: Record onSendFile: (file: File) => void @@ -66,6 +67,7 @@ export default class App extends React.PureComponent { messages, messagesCount, onSendFile, + play, peers, sendMessage, toggleActive, @@ -97,6 +99,7 @@ export default class App extends React.PureComponent { 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 { active={userId === active} key={userId} onClick={toggleActive} + play={play} stream={streams[userId]} userId={userId} videos={videos} diff --git a/src/client/components/Media.test.tsx b/src/client/components/Media.test.tsx index 7fac5d5..eaeffad 100644 --- a/src/client/components/Media.test.tsx +++ b/src/client/components/Media.test.tsx @@ -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() diff --git a/src/client/components/Media.tsx b/src/client/components/Media.tsx index 3924a5f..caf6d48 100644 --- a/src/client/components/Media.tsx +++ b/src/client/components/Media.tsx @@ -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) { ) +}) + +export interface AutoplayProps { + play: () => void +} + +export const AutoplayMessage = React.memo( + function Autoplay(props: AutoplayProps) { + return ( +
+ The browser has blocked video autoplay on this page. + To continue with your call, please press the play button: +   + +
+ ) + }, +) + +export const Media = c(React.memo(function Media(props: MediaProps) { + return ( +
+ {props.autoplayError && } + +
+ ) })) interface OptionsProps { diff --git a/src/client/components/Video.test.tsx b/src/client/components/Video.test.tsx index b95f487..e07737f 100644 --- a/src/client/components/Video.test.tsx +++ b/src/client/components/Video.test.tsx @@ -12,6 +12,8 @@ describe('components/Video', () => { stream: null | AddStreamPayload } + const play = jest.fn() + class VideoWrapper extends React.PureComponent { ref = React.createRef