Add compatibility layer for iOS 11
This commit is contained in:
parent
967d623b32
commit
cc1639eade
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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: {}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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 () {
|
||||
|
||||
@ -45,6 +45,7 @@ export default class Input extends React.PureComponent {
|
||||
type="text"
|
||||
value={message}
|
||||
/>
|
||||
<input type="submit" value="Send"/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
76
src/client/components/__tests__/Video-test.js
Normal file
76
src/client/components/__tests__/Video-test.js
Normal 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 }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"] {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user