Skip to content
154 changes: 154 additions & 0 deletions beta/airconsole-1.11.0.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ AirConsole.VIBRATE = {
}
};

/**
* TODO
*/
AirConsole.MEDIA_PERMISSION_DENIED = {
temporary: "temporary",
permanent: "permanent",
};

/** ------------------------------------------------------------------------ *
* @chapter CONNECTIVITY *
* @see http://developers.airconsole.com/#!/guides/pong *
Expand Down Expand Up @@ -691,6 +699,98 @@ AirConsole.prototype.vibrate = function(options) {
this.set_("vibrate", options);
};


/** ------------------------------------------------------------------------ *
* @chapter MICROPHONE PERMISSION *
* ------------------------------------------------------------------------- */

/**
* Gets called on the game screen when a controller is granted user media
* access as a result of calling getUserMedia on that controller.
* @abstract
* @param {number} device_id - The device_id of the controller that was granted access.
*/
AirConsole.prototype.onUserMediaAccessGranted = function(device_id) {};

/**
* @typedef {string} AirConsole~MicDenialReason
* @enum {string}
* @property {string} DENIED_BY_USER - The user denied the permission request.
* @property {string} NOT_SUPPORTED - The browser does not support the requested media type.
* @property {string} AIRCONSOLE_NOT_READY - The controller called getUserMedia before onReady was called or after the device got disconnected.
* @property {string} REQUEST_PENDING - The controller called getUserMedia while another getUserMedia request is still pending.
* @property {string} INVALID_CONSTRAINTS - The controller called getUserMedia with invalid constraints (e.g. no audio or video constraint specified).
* @property {string} TIMEOUT - The user did not respond to the permission request in time.
*/

/**
* Gets called on the game screen when user media access is denied or
* subsequently lost for a controller that called getUserMedia.
* @abstract
* @param {number} device_id - The device_id of the controller.
* @param {AirConsole~MicDenialReason} reason - The reason for denial or loss.
*/
AirConsole.prototype.onUserMediaAccessDenied = function(device_id, reason) {};

/**
* @typedef {Object} AirConsole~GetUserMediaConstraint
* @property {boolean} audio - Whether to request audio permissions.
* @property {boolean | object} video - True, to use default camera video stream or specific object following the
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia getUserMedia constraints}.
*/

/**
* Requests media permissions (e.g. microphone) for the controller.
* Can only be called by a controller (not the screen).
* @param {Object.<AirConsole~GetUserMediaConstraint>} constraints - User Media Request constraints
* Currently only 'audio' is supported.
* @return {Promise.<{success: boolean, stream: MediaStream=, error: Error=}>}
*/
AirConsole.prototype.getUserMedia = function getUserMedia(constraints) {
var me = this;
return new Promise(function(resolve) {
if (me.device_id === AirConsole.SCREEN) {
resolve({ success: false, error: new Error('getUserMedia failed: getUserMedia is not supported on screen') });
return;
}
if (me.device_id === undefined || me.device_id === null) {
resolve({ success: false, error: new Error('getUserMedia failed: AirConsole not ready') });
return;
}
if (me.media_permission_pending_) {
resolve({ success: false, error: new Error('getUserMedia failed: Request already in progress') });
return;
}
if (!constraints || !(constraints.hasOwnProperty('audio') || constraints.hasOwnProperty('video'))) {
resolve({ success: false, error: new Error('getUserMedia failed: audio or video constraint must be specified') });
return;
}
me.media_permission_constraints_ = constraints;
me.media_permission_pending_ = true;
me.media_permission_resolve_ = resolve;
me.media_permission_timeout_ = setTimeout(function() {
me._resolveMediaPermission_({ success: false, error: new Error('timeout') });
}, 30000);

// Send the request to the platform to decide where and how the user media request needs to take place based on
// browser or controller environment.
me.sendEvent_('requestUserMediaPermission', { constraints: constraints });
});
};

AirConsole.prototype._resolveMediaPermission_ = function _resolveMediaPermission_(result) {
clearTimeout(this.media_permission_timeout_);
this.media_permission_pending_ = false;
this.media_permission_constraints_ = undefined;
this.resolveMediaPermissionError_ = undefined;
this.media_permission_timeout_ = undefined;
const resolve = this.media_permission_resolve_;
this.media_permission_resolve_ = undefined;
if (resolve) {
resolve(result);
}
};

/** ------------------------------------------------------------------------ *
* @chapter ADS *
* ------------------------------------------------------------------------- */
Expand Down Expand Up @@ -1355,6 +1455,16 @@ AirConsole.prototype.onPostMessage_ = function(event) {
if (data.device_data._is_profile_update) {
me.onDeviceProfileChange(sender);
}
if (data.device_data._is_userMediaPermission_update) {
if (!!data.device_data.userMediaPermission) {
const { granted, reason } = data.device_data.userMediaPermission;
if (granted) {
me.onUserMediaAccessGranted(sender);
} else {
me.onUserMediaAccessDenied(sender, reason);
}
}
}
}
}
} else if (data.action === "ready") {
Expand Down Expand Up @@ -1453,6 +1563,40 @@ AirConsole.prototype.onPostMessage_ = function(event) {
}
} else if (data.action === 'setGameSafeArea') {
me.onSetSafeArea(data.gameSafeArea);
} else if (data.action === 'event') {
const { type } = data;

if (type === 'userMediaPermissionDenied') {
const reason = data.data?.reason;

me._resolveMediaPermission_({
success: false,
reason,
error: me.resolveMediaPermissionError_
});
} else if (type === 'userMediaPermissionGranted' || type === 'promptUserMediaPermission') {
const userPromptStartTime = performance.now();

navigator.mediaDevices.getUserMedia(me.media_permission_constraints_).then(
function success(stream) {
me._resolveMediaPermission_({ success: true, stream: stream });
me.sendEvent_('userMediaPermissionGranted', {});
},
function failure(error) {
// Native controller
if (type === 'userMediaPermissionGranted') {
me._resolveMediaPermission_({ success: false, error });
} else if (type === 'promptUserMediaPermission') {
me.resolveMediaPermissionError_ = error;
// Web based controller
const userPromptDuration = performance.now() - userPromptStartTime;
if (error.name === 'NotAllowedError') {
me.sendEvent_('userMediaPermissionDenied', { userPromptDuration });
}
}
}
);
}
}
};

Expand Down Expand Up @@ -1526,6 +1670,16 @@ AirConsole.prototype.set_ = function(key, value) {
AirConsole.postMessage_({ action: "set", key: key, value: value });
};

/**
* Sends an event to the external AirConsole framework.
* @param {string} eventType - The type of the event.
* @param {serializable} eventData - The data of the event. Must be serializable.
* @private
*/
AirConsole.prototype.sendEvent_ = function(eventType, eventData) {
AirConsole.postMessage_({ action: 'event', type: eventType, data: eventData });
};

/**
* Adds default css rules to documents so nothing is selectable, zoom is
* fixed to 1 and preventing scrolling down (iOS 8 clients drop out of
Expand Down
1 change: 1 addition & 0 deletions tests/airconsole-1.11.0-spec.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<script src="spec/methods/spec-pause.js"></script>
<script src="spec/methods/spec-immersive.js"></script>
<script src="spec/methods/spec-player-silencing.js"></script>
<script src="spec/methods/spec-usermedia-permissions.js"></script>

<!-- include spec files here... -->
<script src="spec/airconsole-1.11.0-spec.js"></script>
Expand Down
13 changes: 13 additions & 0 deletions tests/spec/airconsole-1.11.0-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,4 +384,17 @@ describe("AirConsole 1.11.0", function () {

testAirConsole110Plus();
});

/**
======================================================================================
TEST MEDIA PERMISSIONS FUNCTIONALITY
*/

describe("User Media Permissions", function () {
afterEach(function () {
tearDown();
});

testUserMediaPermissions();
});
});
Loading