Add actions/MediaActions and reducers/media
This commit is contained in:
parent
46eeae04fc
commit
6fd6a4edf3
106
src/client/actions/MediaActions.ts
Normal file
106
src/client/actions/MediaActions.ts
Normal 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
|
||||||
@ -15,6 +15,11 @@ export const NOTIFY_CLEAR = 'NOTIFY_CLEAR'
|
|||||||
|
|
||||||
export const MESSAGE_ADD = 'MESSAGE_ADD'
|
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_ADD = 'PEER_ADD'
|
||||||
export const PEER_REMOVE = 'PEER_REMOVE'
|
export const PEER_REMOVE = 'PEER_REMOVE'
|
||||||
export const PEERS_DESTROY = 'PEERS_DESTROY'
|
export const PEERS_DESTROY = 'PEERS_DESTROY'
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import alerts from './alerts'
|
|||||||
import notifications from './notifications'
|
import notifications from './notifications'
|
||||||
import messages from './messages'
|
import messages from './messages'
|
||||||
import peers from './peers'
|
import peers from './peers'
|
||||||
|
import media from './media'
|
||||||
import streams from './streams'
|
import streams from './streams'
|
||||||
import { combineReducers } from 'redux'
|
import { combineReducers } from 'redux'
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export default combineReducers({
|
|||||||
alerts,
|
alerts,
|
||||||
notifications,
|
notifications,
|
||||||
messages,
|
messages,
|
||||||
|
media,
|
||||||
peers,
|
peers,
|
||||||
streams,
|
streams,
|
||||||
})
|
})
|
||||||
|
|||||||
132
src/client/reducers/media.test.ts
Normal file
132
src/client/reducers/media.test.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
65
src/client/reducers/media.ts
Normal file
65
src/client/reducers/media.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user