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 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'
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
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