Add actions/MediaActions and reducers/media

This commit is contained in:
Jerko Steiner 2019-11-16 12:22:37 -03:00
parent 46eeae04fc
commit 6fd6a4edf3
5 changed files with 310 additions and 0 deletions

View File

@ -0,0 +1,106 @@
import { makeAction, AsyncAction } from '../async'
import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM } from '../constants'
export interface MediaDevice {
id: string
name: string
type: 'audioinput' | 'videoinput'
}
export const enumerateDevices = makeAction(MEDIA_ENUMERATE, async () => {
const devices = await navigator.mediaDevices.enumerateDevices()
return devices
.filter(
device => device.kind === 'audioinput' || device.kind === 'videoinput')
.map(device => ({
id: device.deviceId,
type: device.kind,
name: device.label,
}) as MediaDevice)
})
export type FacingMode = 'user' | 'environment'
export interface DeviceConstraint {
deviceId: string
}
export interface FacingConstraint {
facingMode: FacingMode | { exact: FacingMode }
}
export type VideoConstraint = DeviceConstraint | boolean | FacingConstraint
export type AudioConstraint = DeviceConstraint | boolean
export interface GetMediaConstraints {
video: VideoConstraint
audio: AudioConstraint
}
declare global {
interface Navigator {
webkitGetUserMedia?: typeof navigator.getUserMedia
mozGetUserMedia?: typeof navigator.getUserMedia
}
}
async function getUserMedia(
constraints: MediaStreamConstraints,
): Promise<MediaStream> {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
return navigator.mediaDevices.getUserMedia(constraints)
}
const _getUserMedia: typeof navigator.getUserMedia =
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia
return new Promise<MediaStream>((resolve, reject) => {
_getUserMedia.call(navigator, constraints, resolve, reject)
})
}
export interface MediaVideoConstraintAction {
type: 'MEDIA_VIDEO_CONSTRAINT_SET'
payload: VideoConstraint
}
export interface MediaAudioConstraintAction {
type: 'MEDIA_AUDIO_CONSTRAINT_SET'
payload: AudioConstraint
}
export function setVideoConstraint(
payload: VideoConstraint,
): MediaVideoConstraintAction {
return {
type: MEDIA_VIDEO_CONSTRAINT_SET,
payload,
}
}
export function setAudioConstraint(
payload: AudioConstraint,
): MediaAudioConstraintAction {
return {
type: MEDIA_AUDIO_CONSTRAINT_SET,
payload,
}
}
export const getMediaStream = makeAction(
MEDIA_STREAM,
async (constraints: GetMediaConstraints) => getUserMedia(constraints),
)
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', MediaStream>
export type MediaAction =
MediaVideoConstraintAction |
MediaAudioConstraintAction |
MediaEnumerateAction |
MediaStreamAction

View File

@ -15,6 +15,11 @@ export const NOTIFY_CLEAR = 'NOTIFY_CLEAR'
export const MESSAGE_ADD = 'MESSAGE_ADD'
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 PEER_ADD = 'PEER_ADD'
export const PEER_REMOVE = 'PEER_REMOVE'
export const PEERS_DESTROY = 'PEERS_DESTROY'

View File

@ -3,6 +3,7 @@ import alerts from './alerts'
import notifications from './notifications'
import messages from './messages'
import peers from './peers'
import media from './media'
import streams from './streams'
import { combineReducers } from 'redux'
@ -11,6 +12,7 @@ export default combineReducers({
alerts,
notifications,
messages,
media,
peers,
streams,
})

View File

@ -0,0 +1,132 @@
import * as MediaActions from '../actions/MediaActions'
import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM } from '../constants'
import { createStore, Store } from '../store'
describe('media', () => {
let store: Store
beforeEach(() => {
store = createStore();
(navigator as any).mediaDevices = {}
})
afterEach(() => {
delete (navigator as any).mediaDevices
})
function toJSON(this: MediaDeviceInfo) {
return JSON.stringify(this)
}
describe(MEDIA_ENUMERATE, () => {
beforeEach(() => {
navigator.mediaDevices.enumerateDevices = async () => {
const result: MediaDeviceInfo[] = [{
deviceId: 'abcdef1',
groupId: 'group1',
kind: 'videoinput',
label: 'Video Input',
toJSON,
}, {
deviceId: 'abcdef2',
groupId: 'group1',
kind: 'audioinput',
label: 'Audio Input',
toJSON,
}, {
deviceId: 'abcdef3',
groupId: 'group2',
kind: 'audiooutput',
label: 'Audio Output',
toJSON,
}]
return result
}
})
it('retrieves a list of audioinput/videoinput devices', async () => {
await store.dispatch(MediaActions.enumerateDevices())
expect(store.getState().media.devices).toEqual([{
id: 'abcdef1',
name: 'Video Input',
type: 'videoinput',
}, {
id: 'abcdef2',
name: 'Audio Input',
type: 'audioinput',
}])
})
it('handles errors', async () => {
delete navigator.mediaDevices.enumerateDevices
try {
await store.dispatch(MediaActions.enumerateDevices())
} catch (err) {
// do nothing
}
expect(store.getState().media.devices).toEqual([])
expect(store.getState().media.error).toBeTruthy()
})
})
describe(MEDIA_VIDEO_CONSTRAINT_SET, () => {
it('sets constraints for video device', () => {
expect(store.getState().media.video).toEqual({ facingMode: 'user' })
const constraint: MediaActions.VideoConstraint = true
store.dispatch(MediaActions.setVideoConstraint(constraint))
expect(store.getState().media.video).toEqual(constraint)
})
})
describe(MEDIA_AUDIO_CONSTRAINT_SET, () => {
it('sets constraints for audio device', () => {
expect(store.getState().media.audio).toBe(true)
const constraint: MediaActions.AudioConstraint = { deviceId: 'abcd' }
store.dispatch(MediaActions.setAudioConstraint(constraint))
expect(store.getState().media.audio).toEqual(constraint)
})
})
describe(MEDIA_STREAM, () => {
const stream: MediaStream = {} as MediaStream
describe('using navigator.mediaDevices.getUserMedia', () => {
beforeEach(() => {
navigator.mediaDevices.getUserMedia = async () => stream
})
it('returns a promise with media stream', async () => {
const promise = MediaActions.getMediaStream({
audio: true,
video: true,
})
expect(promise.type).toBe('MEDIA_STREAM')
expect(promise.status).toBe('pending')
expect(await promise).toBe(stream)
})
});
['getUserMedia', 'mozGetUserMedia', 'webkitGetUserMedia'].forEach(item => {
describe('compatibility: navigator.' + item, () => {
beforeEach(() => {
const getUserMedia: typeof navigator.getUserMedia =
(constraint, onSuccess, onError) => onSuccess(stream);
(navigator as any)[item] = getUserMedia
})
afterEach(() => {
delete (navigator as any)[item]
})
it('returns a promise with media stream' + item, async () => {
const promise = MediaActions.getMediaStream({
audio: true,
video: true,
})
expect(promise.type).toBe('MEDIA_STREAM')
expect(promise.status).toBe('pending')
expect(await promise).toBe(stream)
})
})
})
})
})

View File

@ -0,0 +1,65 @@
import { MediaDevice, AudioConstraint, VideoConstraint, MediaAction, MediaEnumerateAction } from '../actions/MediaActions'
import { MEDIA_ENUMERATE, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET } from '../constants'
export interface MediaState {
devices: MediaDevice[]
video: VideoConstraint
audio: AudioConstraint
loading: boolean
error: string
}
const defaultState: MediaState = {
devices: [],
video: { facingMode: 'user'},
audio: true,
loading: false,
error: '',
}
export function handleEnumerate(
state: MediaState,
action: MediaEnumerateAction,
): MediaState {
switch (action.status) {
case 'resolved':
return {
...state,
loading: false,
devices: action.payload,
}
case 'pending':
return {
...state,
loading: true,
}
case 'rejected':
return {
...state,
loading: false,
error: 'Could not retrieve media devices',
}
}
}
export default function media(
state = defaultState,
action: MediaAction,
): MediaState {
switch (action.type) {
case MEDIA_ENUMERATE:
return handleEnumerate(state, action)
case MEDIA_AUDIO_CONSTRAINT_SET:
return {
...state,
audio: action.payload,
}
case MEDIA_VIDEO_CONSTRAINT_SET:
return {
...state,
video: action.payload,
}
default:
return state
}
}