Skip to content

Commit

Permalink
feat(plugin-phone): add Phone#facingMode, Call#toggleFacingMode()
Browse files Browse the repository at this point in the history
  • Loading branch information
Ian W. Remmel committed Mar 16, 2017
1 parent e593ecd commit 0fd5afb
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 44 deletions.
6 changes: 3 additions & 3 deletions packages/plugin-phone/package.json
Expand Up @@ -12,11 +12,11 @@
"@ciscospark/plugin-metrics": "^0.7.68",
"@ciscospark/spark-core": "^0.7.68",
"babel-runtime": "^6.3.19",
"detectrtc": "^1.3.3",
"detectrtc": "^1.3.4",
"envify": "^3.4.0",
"lodash": "^4.13.1",
"sdp-transform": "^1.6.2",
"webrtc-adapter": "^1.0.7"
"sdp-transform": "^2.3.0",
"webrtc-adapter": "^3.2.0"
},
"devDependencies": {
"@ciscospark/test-helper-chai": "^0.7.34",
Expand Down
129 changes: 106 additions & 23 deletions packages/plugin-phone/src/call.js
Expand Up @@ -13,7 +13,7 @@ import {
USE_INCOMING,
FETCH
} from '@ciscospark/plugin-locus';
import {debounce, defaults, find} from 'lodash';
import {debounce, defaults, find, get, set} from 'lodash';
import {
activeParticipants,
direction,
Expand Down Expand Up @@ -93,6 +93,16 @@ const Call = SparkPlugin.extend({

session: {
correlationId: `string`,
/**
* @instance
* @memberof Call
* @type {string}
* @readonly
*/
facingMode: {
type: `string`,
values: [`user`, `environment`]
},
locus: `object`,
/**
* Returns the local MediaStream for the call. May initially be `null`
Expand All @@ -106,24 +116,24 @@ const Call = SparkPlugin.extend({
* listeners that we are now sending media from a new source.
* @instance
* @memberof Call
* @member {MediaStream}
* @readonly
* @type {MediaStream}
*/
localMediaStream: `object`,
/**
* Object URL that refers to {@link Call#localMediaStream}. Will be
* automatically deallocated when the call ends
* @instance
* @memberof Call
* @member {string}
* @type {string}
* @readonly
*/
localMediaStreamUrl: `string`,
/**
* Object URL that refers to {@link Call#remoteMediaStream}. Will be
* automatically deallocated when the call ends
* @instance
* @memberof Call
* @member {string}
* @type {string}
* @readonly
*/
remoteMediaStreamUrl: `string`
Expand Down Expand Up @@ -394,6 +404,18 @@ const Call = SparkPlugin.extend({
if (this.media.localMediaStream !== this.localMediaStream) {
this.media.localMediaStream = this.localMediaStream;
}


if (this.facingMode) {
const mode = get(this, `media.videoConstraint.facingMode.exact`);
if (mode === `user`) {
this.facingMode = `user`;
}

if (mode === `environment`) {
this.facingMode = `environment`;
}
}
});

[
Expand Down Expand Up @@ -477,7 +499,6 @@ const Call = SparkPlugin.extend({
*/
acknowledge() {
this.logger.info(`call: acknowledging`);
// TODO call this method automatically unless config says otherwise
return this.spark.locus.alert(this.locus)
.then((locus) => this._setLocus(locus))
.then(tap(() => this.logger.info(`call: acknowledged`)));
Expand Down Expand Up @@ -603,6 +624,52 @@ const Call = SparkPlugin.extend({
.then(tap(() => this.logger.info(`call: rejected`)));
},

/**
* Replaces the current mediaStrem with one with identical constraints, except
* for an opposite facing mode. If the current facing mode cannot be
* determined, the facing mode will be set to `user`. If the call is audio
* only, this function will throw.
* @returns {undefined}
*/
toggleFacingMode() {
const constraints = {
audio: Object.assign({}, this.media.audioConstraint),
video: this.media.videoConstraint
};

if (!constraints.video) {
throw new Error(`Cannot toggle facignMode on audio-only call`);
}

if (this.facingMode !== `user` && this.facingMode !== `environment`) {
throw new Error(`Cannot determine current facing mode; specify a new localMediaStream to change cameras`);
}

if (constraints.video === true) {
constraints.video = {
facingMode: {
exact: this.facingMode
}
};
}

if (this.facingMode === `user`) {
set(constraints, `video.facingMode.exact`, `environment`);
}
else {
set(constraints, `video.facingMode.exact`, `user`);
}

return this.spark.phone.createLocalMediaStream(constraints)
.then((stream) => new Promise((resolve) => {
this.media.once(`answeraccepted`, resolve);
this.localMediaStream = stream;
}))
.then(() => {
this.facingMode = constraints.video.facingMode.exact;
});
},

/**
* Starts sending audio to the Cisco Spark Cloud
* @instance
Expand Down Expand Up @@ -735,25 +802,41 @@ const Call = SparkPlugin.extend({
.then((locus) => this._setLocus(locus));
},

_join(locusMethodName, target, options) {
options = options || {};
options.constraints = defaults(options.constraints, {
audio: true,
video: true
});
// The complexity in _join is largely driven up by fairly readable `||`s
// eslint-disable-next-line complexity
_join(locusMethodName, target, options = {}) {
if (options.localMediaStream) {
this.media.set(`localMediaStream`, options.localMediaStream);
}
else {
if (!options.constraints) {
options.constraints = {
audio: true,
video: {
facingMode: {
exact: this.spark.phone.defaultFacingMode
}
}
};
}
const mode = get(options, `constraints.video.facingMode.exact`);
if (mode === `user` || mode === `environment`) {
this.facingMode = mode;
}

const recvOnly = !options.constraints.audio && !options.constraints.video;
options.offerOptions = defaults(options.offerOptions, {
offerToReceiveAudio: recvOnly || options.constraints.audio,
offerToReceiveVideo: recvOnly || options.constraints.video
});
const recvOnly = !options.constraints.audio && !options.constraints.video;
options.offerOptions = defaults(options.offerOptions, {
offerToReceiveAudio: recvOnly || !!options.constraints.audio,
offerToReceiveVideo: recvOnly || !!options.constraints.video
});

this.media.set({
audio: options.constraints.audio,
video: options.constraints.video,
offerToReceiveAudio: options.offerOptions.offerToReceiveAudio,
offerToReceiveVideo: options.offerOptions.offerToReceiveVideo
});
this.media.set({
audio: options.constraints.audio,
video: options.constraints.video,
offerToReceiveAudio: options.offerOptions.offerToReceiveAudio,
offerToReceiveVideo: options.offerOptions.offerToReceiveVideo
});
}

if (!target.correlationId) {
this.correlationId = options.correlationId = uuid.v4();
Expand Down
23 changes: 21 additions & 2 deletions packages/plugin-phone/src/phone.js
Expand Up @@ -66,6 +66,27 @@ const Phone = SparkPlugin.extend({
}
},

session: {
/**
* Specifies the facingMode to be used by {@link Phone#dial} and
* {@link Call#answer} when no constraint is specified. Does not apply if
* - a {@link MediaStream} is passed to {@link Phone#dial} or
* {@link Call#answer}
* - constraints are passed to {@link Phone#dial} or {@link Call#answer}
* The only valid values are `user` and `environment`. For any other values,
* you must provide your own constrains or {@link MediaStream}
* @default `user`
* @instance
* @memberof {Phone}
* @type {string}
*/
defaultFacingMode: {
default: `user`,
type: `string`,
values: [`user`, `environment`]
}
},

namespace: `phone`,

/**
Expand Down Expand Up @@ -138,8 +159,6 @@ const Phone = SparkPlugin.extend({
* @returns {Promise<MediaStream>}
*/
createLocalMediaStream(options) {
// TODO need to figure out a way to manage the stream internally. currently,
// misuse makes it really easy to lock the camera in the on state.
options = options || {};
const constraints = options.constraints || options;
defaults(constraints, {
Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-phone/src/web-rtc-media.js
Expand Up @@ -270,6 +270,11 @@ const WebRTCMedia = AmpState.extend({
if (!this.peer) {
return;
}

if (this.peer.signalingState === `closed`) {
return;
}

const streams = this.peer.getLocalStreams();
if (!streams.includes(this.localMediaStream)) {
streams.forEach((stream) => {
Expand Down
21 changes: 19 additions & 2 deletions packages/plugin-phone/test/integration/spec/call.js
Expand Up @@ -537,11 +537,28 @@ describe(`plugin-phone`, function() {

describe(`#toggleFacingMode`, () => {
describe(`when the facing mode is "user"`, () => {
it(`changes the facing mode to "environment"`);
it(`changes the facing mode to "environment"`, () => handleErrorEvent(spock.spark.phone.dial(mccoy.email), (call) => Promise.all([
mccoy.spark.phone.when(`call:incoming`)
.then(([c]) => c.answer()),
call.when(`connected`)
.then(() => assert.equal(call.facingMode, `user`))
.then(() => call.toggleFacingMode())
.then(() => assert.equal(call.facingMode, `environment`))
])));
});

describe(`when the facing mode is "environment"`, () => {
it(`changes the facing mode to "user"`);
it(`changes the facing mode to "user"`, () => {
spock.spark.phone.defaultFacingMode = `environment`;
return handleErrorEvent(spock.spark.phone.dial(mccoy.email), (call) => Promise.all([
mccoy.spark.phone.when(`call:incoming`)
.then(([c]) => c.answer()),
call.when(`connected`)
.then(() => assert.equal(call.facingMode, `environment`))
.then(() => call.toggleFacingMode())
.then(() => assert.equal(call.facingMode, `user`))
]));
});
});
});

Expand Down
14 changes: 0 additions & 14 deletions packages/plugin-phone/test/integration/spec/phone.js
Expand Up @@ -288,20 +288,6 @@ describe(`plugin-phone`, function() {
it(`is a noop when already registered`, () => assert.isFulfilled(spock.spark.phone.register()));
});

describe(`#defaultFacingMode`, () => {
it.skip(`defaults to user`, () => {
assert.equal(spock.spark.phone.defaultFacingMode, `user`);
});

describe(`when video constraints are not specified`, () => {
it(`gets passed as the video constraint`);
});

describe(`when video constraints are not specified`, () => {
it(`does not get passed as the video constraint`);
});
});

describe(`when a call is received`, () => {
it(`emits a call:incoming event`, () => {
spock.spark.phone.dial(mccoy.email);
Expand Down

0 comments on commit 0fd5afb

Please sign in to comment.