Add compatibility layer for iOS 11

This commit is contained in:
Jerko Steiner 2017-06-20 19:11:51 -04:00
parent 967d623b32
commit cc1639eade
13 changed files with 200 additions and 24 deletions

View File

@ -16,7 +16,7 @@
"js": "browserify -t babelify ./src/client/index.js -o ./build/index.js",
"js:watch": "watchify -d -v -t babelify ./src/client/index.js -o ./build/index.js",
"css": "node-sass ./src/scss/style.scss -o ./build/",
"css:watch": "node-sass --watch ./src/scss/style.scss -o ./build/",
"css:watch": "npm run css && node-sass --watch ./src/scss/style.scss -o ./build/",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"ci": "npm run lint && npm run test:coverage && npm run build"

View File

@ -1,8 +1,10 @@
import Promise from 'bluebird'
export const createObjectURL = object => 'blob://' + String(object)
export const createObjectURL = jest.fn()
.mockImplementation(object => 'blob://' + String(object))
export const revokeObjectURL = jest.fn()
class MediaStream {}
export class MediaStream {}
export function getUserMedia () {
return !getUserMedia.shouldFail
? Promise.resolve(getUserMedia.stream)

View File

@ -1,5 +1,6 @@
jest.mock('../actions/CallActions.js')
jest.mock('../socket.js')
jest.mock('../window.js')
import * as constants from '../constants.js'
import App from '../containers/App.js'
@ -8,6 +9,7 @@ import ReactDOM from 'react-dom'
import TestUtils from 'react-dom/test-utils'
import configureStore from 'redux-mock-store'
import reducers from '../reducers'
import { MediaStream } from '../window.js'
import { Provider } from 'react-redux'
import { init } from '../actions/CallActions.js'
import { middlewares } from '../store.js'
@ -44,9 +46,12 @@ describe('App', () => {
describe('state', () => {
let alert
beforeEach(() => {
state.streams = state.streams.merge({
test: 'blob://'
})
state.streams = {
test: {
mediaStream: new MediaStream(),
url: 'blob://'
}
}
state.peers = {
test: {}
}

View File

@ -2,6 +2,7 @@ import Promise from 'bluebird'
import {
createObjectURL,
revokeObjectURL,
getUserMedia,
navigator,
play,
@ -100,6 +101,15 @@ describe('window', () => {
})
describe('createObjectURL', () => {
it('calls window.URL.revokeObjectURL', () => {
window.URL.revokeObjectURL = jest.fn()
expect(revokeObjectURL()).toBe(undefined)
})
})
describe('valueOf', () => {
let input

View File

@ -124,7 +124,10 @@ describe('SocketActions', () => {
peer.emit(constants.PEER_EVENT_STREAM, stream)
expect(store.getState().streams).toEqual({
b: jasmine.any(String)
b: {
mediaStream: stream,
url: jasmine.any(String)
}
})
})
})
@ -134,7 +137,10 @@ describe('SocketActions', () => {
const stream = {}
peer.emit(constants.PEER_EVENT_STREAM, stream)
expect(store.getState().streams).toEqual({
b: jasmine.any(String)
b: {
mediaStream: stream,
url: jasmine.any(String)
}
})
})

View File

@ -4,7 +4,7 @@ import Input from './Input.js'
import Notifications, { NotificationPropTypes } from './Notifications.js'
import PropTypes from 'prop-types'
import React from 'react'
import Video from './Video.js'
import Video, { StreamPropType } from './Video.js'
import _ from 'underscore'
export default class App extends React.PureComponent {
@ -17,7 +17,7 @@ export default class App extends React.PureComponent {
notify: PropTypes.func.isRequired,
peers: PropTypes.object.isRequired,
sendMessage: PropTypes.func.isRequired,
streams: PropTypes.objectOf(PropTypes.string).isRequired,
streams: PropTypes.objectOf(StreamPropType).isRequired,
toggleActive: PropTypes.func.isRequired
}
componentDidMount () {

View File

@ -45,6 +45,7 @@ export default class Input extends React.PureComponent {
type="text"
value={message}
/>
<input type="submit" value="Send"/>
</form>
)
}

View File

@ -2,12 +2,18 @@ import PropTypes from 'prop-types'
import React from 'react'
import classnames from 'classnames'
import { ME } from '../constants.js'
import { MediaStream } from '../window.js'
export const StreamPropType = PropTypes.shape({
mediaStream: PropTypes.instanceOf(MediaStream).isRequired,
url: PropTypes.string
})
export default class Video extends React.PureComponent {
static propTypes = {
onClick: PropTypes.func,
active: PropTypes.bool.isRequired,
stream: PropTypes.string,
stream: StreamPropType,
userId: PropTypes.string.isRequired
}
handleClick = e => {
@ -19,8 +25,26 @@ export default class Video extends React.PureComponent {
e.preventDefault()
e.target.play()
}
componentDidMount () {
this.componentDidUpdate()
}
componentDidUpdate () {
const { stream } = this.props
const { video } = this.refs
const mediaStream = stream && stream.mediaStream
const url = stream && stream.url
if ('srcObject' in video) {
if (video.srcObject !== mediaStream) {
this.refs.video.srcObject = mediaStream
}
} else {
if (video.src !== url) {
video.src = url
}
}
}
render () {
const { active, stream, userId } = this.props
const { active, userId } = this.props
const className = classnames('video-container', { active })
return (
<div className={className}>
@ -28,7 +52,7 @@ export default class Video extends React.PureComponent {
muted={userId === ME}
onClick={this.handleClick}
onLoadedMetadata={this.play}
src={stream}
ref="video"
/>
</div>
)

View File

@ -0,0 +1,76 @@
jest.mock('../../window.js')
import React from 'react'
import TestUtils from 'react-dom/test-utils'
import Video from '../Video.js'
import { MediaStream } from '../../window.js'
describe('components/Video', () => {
class VideoWrapper extends React.PureComponent {
static propTypes = Video.propTypes
constructor () {
super()
this.state = {}
}
render () {
return <Video
active={this.props.active}
stream={this.state.stream || this.props.stream}
onClick={this.props.onClick}
userId="test"
/>
}
}
let component, video, onClick, mediaStream, url
function render () {
onClick = jest.fn()
mediaStream = new MediaStream()
component = TestUtils.renderIntoDocument(
<VideoWrapper
active
stream={{ mediaStream, url }}
onClick={onClick}
userId="test"
/>
)
video = TestUtils.findRenderedComponentWithType(component, Video)
}
describe('render', () => {
it('should not fail', () => {
render()
})
})
describe('componentDidUpdate', () => {
describe('src', () => {
beforeEach(() => {
render()
delete video.refs.video.srcObject
})
it('updates src only when changed', () => {
mediaStream = new MediaStream()
component.setState({
stream: { url: 'test', mediaStream }
})
expect(video.refs.video.src).toBe('test')
component.setState({
stream: { url: 'test', mediaStream }
})
})
it('updates srcObject only when changed', () => {
video.refs.video.srcObject = null
mediaStream = new MediaStream()
component.setState({
stream: { url: 'test', mediaStream }
})
expect(video.refs.video.srcObject).toBe(mediaStream)
component.setState({
stream: { url: 'test', mediaStream }
})
})
})
})
})

View File

@ -1,13 +1,14 @@
jest.mock('../../window.js')
import * as StreamActions from '../../actions/StreamActions.js'
import reducers from '../index.js'
import { MediaStream } from '../../window.js'
import { applyMiddleware, createStore } from 'redux'
import { create } from '../../middlewares.js'
import reducers from '../index.js'
import { createObjectURL } from '../../window.js'
describe('reducers/alerts', () => {
class MediaStream {}
let store, stream, userId
beforeEach(() => {
store = createStore(
@ -18,6 +19,11 @@ describe('reducers/alerts', () => {
stream = new MediaStream()
})
afterEach(() => {
createObjectURL
.mockImplementation(object => 'blob://' + String(object))
})
describe('defaultState', () => {
it('should have default state set', () => {
expect(store.getState().streams).toEqual({})
@ -28,7 +34,21 @@ describe('reducers/alerts', () => {
it('adds a stream', () => {
store.dispatch(StreamActions.addStream({ userId, stream }))
expect(store.getState().streams).toEqual({
[userId]: jasmine.any(String)
[userId]: {
mediaStream: stream,
url: jasmine.any(String)
}
})
})
it('does not fail when createObjectURL fails', () => {
createObjectURL
.mockImplementation(() => { throw new Error('test') })
store.dispatch(StreamActions.addStream({ userId, stream }))
expect(store.getState().streams).toEqual({
[userId]: {
mediaStream: stream,
url: null
}
})
})
})
@ -39,6 +59,9 @@ describe('reducers/alerts', () => {
store.dispatch(StreamActions.removeStream(userId))
expect(store.getState().streams).toEqual({})
})
it('does not fail when no stream', () => {
store.dispatch(StreamActions.removeStream(userId))
})
})
})

View File

@ -1,17 +1,39 @@
import * as constants from '../constants.js'
import Immutable from 'seamless-immutable'
import { createObjectURL } from '../window.js'
import _ from 'underscore'
import { createObjectURL, revokeObjectURL } from '../window.js'
import _debug from 'debug'
const defaultState = Immutable({})
const debug = _debug('peercalls')
const defaultState = Object.freeze({})
function safeCreateObjectURL (stream) {
try {
return createObjectURL(stream)
} catch (err) {
debug('Error using createObjectURL: %s', err.message)
return null
}
}
function addStream (state, action) {
const { userId, stream } = action.payload
return state.merge({
[userId]: createObjectURL(stream)
return Object.freeze({
...state,
[userId]: Object.freeze({
mediaStream: stream,
url: safeCreateObjectURL(stream)
})
})
}
const removeStream = (state, action) => state.without(action.payload.userId)
function removeStream (state, action) {
const { userId } = action.payload
const stream = state[userId]
if (stream && stream.url) {
revokeObjectURL(stream.url)
}
return Object.freeze(_.omit(state, [userId]))
}
export default function streams (state = defaultState, action) {
switch (action && action.type) {

View File

@ -16,6 +16,7 @@ export function getUserMedia (constraints) {
}
export const createObjectURL = object => window.URL.createObjectURL(object)
export const revokeObjectURL = url => window.URL.revokeObjectURL(url)
export const navigator = window.navigator
@ -38,3 +39,5 @@ export const valueOf = id => {
export const callId = valueOf('callId')
export const iceServers = JSON.parse(valueOf('iceServers'))
export const MediaStream = window.MediaStream

View File

@ -265,13 +265,17 @@ body.call {
input {
box-shadow: 0px 0px 5px black;
background-color: black;
background-color: rgba(0, 0, 0, 0.5);
// background-color: black;
background-color: #333;
border: none;
color: #ccc;
padding: 0.5rem;
font-family: $font-monospace;
}
input[type="submit"] {
}
}
}