blob: c83ee38fa65d984d9ec2587282e2dbe02f816418 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function createDataChannel(pc, label, onMessage) {
console.log('creating data channel: ' + label);
let dataChannel = pc.createDataChannel(label);
// Return an object with a send function like that of the dataChannel, but
// that only actually sends over the data channel once it has connected.
return {
channelPromise: new Promise((resolve, reject) => {
dataChannel.onopen = (event) => {
resolve(dataChannel);
};
dataChannel.onclose = () => {
console.log(
'Data channel=' + label + ' state=' + dataChannel.readyState);
};
dataChannel.onmessage = onMessage ? onMessage : (msg) => {
console.log('Data channel=' + label + ' data="' + msg.data + '"');
};
dataChannel.onerror = err => {
reject(err);
};
}),
send: function(msg) {
this.channelPromise = this.channelPromise.then(channel => {
channel.send(msg);
return channel;
})
},
};
}
function awaitDataChannel(pc, label, onMessage) {
console.log('expecting data channel: ' + label);
// Return an object with a send function like that of the dataChannel, but
// that only actually sends over the data channel once it has connected.
return {
channelPromise: new Promise((resolve, reject) => {
let prev_ondatachannel = pc.ondatachannel;
pc.ondatachannel = ev => {
let dataChannel = ev.channel;
if (dataChannel.label == label) {
dataChannel.onopen = (event) => {
resolve(dataChannel);
};
dataChannel.onclose = () => {
console.log(
'Data channel=' + label + ' state=' + dataChannel.readyState);
};
dataChannel.onmessage = onMessage ? onMessage : (msg) => {
console.log('Data channel=' + label + ' data="' + msg.data + '"');
};
dataChannel.onerror = err => {
reject(err);
};
} else if (prev_ondatachannel) {
prev_ondatachannel(ev);
}
};
}),
send: function(msg) {
this.channelPromise = this.channelPromise.then(channel => {
channel.send(msg);
return channel;
})
},
};
}
class DeviceConnection {
constructor(pc, control, media_stream) {
this._pc = pc;
this._control = control;
this._media_stream = media_stream;
// Disable the microphone by default
this.useMic(false);
this.useVideo(false);
this._cameraDataChannel = pc.createDataChannel('camera-data-channel');
this._cameraDataChannel.binaryType = 'arraybuffer'
this._cameraInputQueue = new Array();
var self = this;
this._cameraDataChannel.onbufferedamountlow = () => {
if (self._cameraInputQueue.length > 0) {
self.sendCameraData(self._cameraInputQueue.shift());
}
}
this._inputChannel = createDataChannel(pc, 'input-channel');
this._adbChannel = createDataChannel(pc, 'adb-channel', (msg) => {
if (this._onAdbMessage) {
this._onAdbMessage(msg.data);
} else {
console.error('Received unexpected ADB message');
}
});
this._controlChannel = awaitDataChannel(pc, 'device-control', (msg) => {
if (this._onControlMessage) {
this._onControlMessage(msg);
} else {
console.error('Received unexpected Control message');
}
});
this._bluetoothChannel = createDataChannel(pc, 'bluetooth-channel', (msg) => {
if (this._onBluetoothMessage) {
this._onBluetoothMessage(msg.data);
} else {
console.error('Received unexpected Bluetooth message');
}
});
this.sendCameraResolution();
this._streams = {};
this._streamPromiseResolvers = {};
pc.addEventListener('track', e => {
console.log('Got remote stream: ', e);
for (const stream of e.streams) {
this._streams[stream.id] = stream;
if (this._streamPromiseResolvers[stream.id]) {
for (let resolver of this._streamPromiseResolvers[stream.id]) {
resolver();
}
delete this._streamPromiseResolvers[stream.id];
}
}
});
}
set description(desc) {
this._description = desc;
}
get description() {
return this._description;
}
get imageCapture() {
if (this._media_stream) {
const track = this._media_stream.getVideoTracks()[0]
return new ImageCapture(track);
}
return undefined;
}
get cameraWidth() {
return this._x_res;
}
get cameraHeight() {
return this._y_res;
}
get cameraEnabled() {
if (this._media_stream) {
return this._media_stream.getVideoTracks().some(track => track.enabled);
}
}
getStream(stream_id) {
return new Promise((resolve, reject) => {
if (this._streams[stream_id]) {
resolve(this._streams[stream_id]);
} else {
if (!this._streamPromiseResolvers[stream_id]) {
this._streamPromiseResolvers[stream_id] = [];
}
this._streamPromiseResolvers[stream_id].push(resolve);
}
});
}
_sendJsonInput(evt) {
this._inputChannel.send(JSON.stringify(evt));
}
sendMousePosition({x, y, down, display_label}) {
this._sendJsonInput({
type: 'mouse',
down: down ? 1 : 0,
x,
y,
display_label,
});
}
// TODO (b/124121375): This should probably be an array of pointer events and
// have different properties.
sendMultiTouch({idArr, xArr, yArr, down, slotArr, display_label}) {
this._sendJsonInput({
type: 'multi-touch',
id: idArr,
x: xArr,
y: yArr,
down: down ? 1 : 0,
slot: slotArr,
display_label: display_label,
});
}
sendKeyEvent(code, type) {
this._sendJsonInput({type: 'keyboard', keycode: code, event_type: type});
}
disconnect() {
this._pc.close();
}
// Sends binary data directly to the in-device adb daemon (skipping the host)
sendAdbMessage(msg) {
this._adbChannel.send(msg);
}
// Provide a callback to receive data from the in-device adb daemon
onAdbMessage(cb) {
this._onAdbMessage = cb;
}
// Send control commands to the device
sendControlMessage(msg) {
this._controlChannel.send(msg);
}
useMic(in_use) {
if (this._media_stream) {
this._media_stream.getAudioTracks().forEach(track => track.enabled = in_use);
}
}
useVideo(in_use) {
if (this._media_stream) {
this._media_stream.getVideoTracks().forEach(track => track.enabled = in_use);
}
}
sendCameraResolution() {
if (this._media_stream) {
const cameraTracks = this._media_stream.getVideoTracks();
if (cameraTracks.length > 0) {
const settings = cameraTracks[0].getSettings();
this._x_res = settings.width;
this._y_res = settings.height;
this.sendControlMessage(JSON.stringify({
command: 'camera_settings',
width: settings.width,
height: settings.height,
frame_rate: settings.frameRate,
facing: settings.facingMode
}));
}
}
}
sendOrQueueCameraData(data) {
if (this._cameraDataChannel.bufferedAmount > 0 || this._cameraInputQueue.length > 0) {
this._cameraInputQueue.push(data);
} else {
this.sendCameraData(data);
}
}
sendCameraData(data) {
const MAX_SIZE = 65535;
const END_MARKER = 'EOF';
for (let i = 0; i < data.byteLength; i += MAX_SIZE) {
// range is clamped to the valid index range
this._cameraDataChannel.send(data.slice(i, i + MAX_SIZE));
}
this._cameraDataChannel.send(END_MARKER);
}
// Provide a callback to receive control-related comms from the device
onControlMessage(cb) {
this._onControlMessage = cb;
}
sendBluetoothMessage(msg) {
this._bluetoothChannel.send(msg);
}
onBluetoothMessage(cb) {
this._onBluetoothMessage = cb;
}
// Provide a callback to receive connectionstatechange states.
onConnectionStateChange(cb) {
this._pc.addEventListener(
'connectionstatechange',
evt => cb(this._pc.connectionState));
}
}
class WebRTCControl {
constructor({
wsUrl = '',
}) {
/*
* Private attributes:
*
* _wsPromise: promises the underlying websocket, should resolve when the
* socket passes to OPEN state, will be rejecte/replaced by a
* rejected promise if an error is detected on the socket.
*
* _onOffer
* _onIceCandidate
*/
this._promiseResolvers = {};
this._wsPromise = new Promise((resolve, reject) => {
let ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.info(`Connected to ${wsUrl}`);
resolve(ws);
};
ws.onerror = evt => {
console.error('WebSocket error:', evt);
reject(evt);
// If the promise was already resolved the previous line has no effect
this._wsPromise = Promise.reject(new Error(evt));
};
ws.onmessage = e => {
let data = JSON.parse(e.data);
this._onWebsocketMessage(data);
};
});
}
_onWebsocketMessage(message) {
const type = message.message_type;
if (message.error) {
console.error(message.error);
this._on_connection_failed(message.error);
return;
}
switch (type) {
case 'config':
this._infra_config = message;
break;
case 'device_info':
if (this._on_device_available) {
this._on_device_available(message.device_info);
delete this._on_device_available;
} else {
console.error('Received unsolicited device info');
}
break;
case 'device_msg':
this._onDeviceMessage(message.payload);
break;
default:
console.error('Unrecognized message type from server: ', type);
this._on_connection_failed('Unrecognized message type from server: ' + type);
console.error(message);
}
}
_onDeviceMessage(message) {
let type = message.type;
switch (type) {
case 'offer':
if (this._onOffer) {
this._onOffer({type: 'offer', sdp: message.sdp});
} else {
console.error('Receive offer, but nothing is wating for it');
}
break;
case 'ice-candidate':
if (this._onIceCandidate) {
this._onIceCandidate(new RTCIceCandidate({
sdpMid: message.mid,
sdpMLineIndex: message.mLineIndex,
candidate: message.candidate
}));
} else {
console.error('Received ice candidate but nothing is waiting for it');
}
break;
default:
console.error('Unrecognized message type from device: ', type);
}
}
async _wsSendJson(obj) {
let ws = await this._wsPromise;
return ws.send(JSON.stringify(obj));
}
async _sendToDevice(payload) {
this._wsSendJson({message_type: 'forward', payload});
}
onOffer(cb) {
this._onOffer = cb;
}
onIceCandidate(cb) {
this._onIceCandidate = cb;
}
async requestDevice(device_id) {
return new Promise((resolve, reject) => {
this._on_device_available = (deviceInfo) => resolve({
deviceInfo,
infraConfig: this._infra_config,
});
this._on_connection_failed = (error) => reject(error);
this._wsSendJson({
message_type: 'connect',
device_id,
});
});
}
ConnectDevice() {
console.log('ConnectDevice');
this._sendToDevice({type: 'request-offer'});
}
/**
* Sends a remote description to the device.
*/
async sendClientDescription(desc) {
console.log('sendClientDescription');
this._sendToDevice({type: 'answer', sdp: desc.sdp});
}
/**
* Sends an ICE candidate to the device
*/
async sendIceCandidate(candidate) {
this._sendToDevice({type: 'ice-candidate', candidate});
}
}
function createPeerConnection(infra_config) {
let pc_config = {iceServers: []};
for (const stun of infra_config.ice_servers) {
pc_config.iceServers.push({urls: 'stun:' + stun});
}
let pc = new RTCPeerConnection(pc_config);
pc.addEventListener('icecandidate', evt => {
console.log('Local ICE Candidate: ', evt.candidate);
});
pc.addEventListener('iceconnectionstatechange', evt => {
console.log(`ICE State Change: ${pc.iceConnectionState}`);
});
pc.addEventListener(
'connectionstatechange',
evt =>
console.log(`WebRTC Connection State Change: ${pc.connectionState}`));
return pc;
}
export async function Connect(deviceId, options) {
let control = new WebRTCControl(options);
let requestRet = await control.requestDevice(deviceId);
let deviceInfo = requestRet.deviceInfo;
let infraConfig = requestRet.infraConfig;
console.log('Device available:');
console.log(deviceInfo);
let pc_config = {iceServers: []};
if (infraConfig.ice_servers && infraConfig.ice_servers.length > 0) {
for (const server of infraConfig.ice_servers) {
pc_config.iceServers.push(server);
}
}
let pc = createPeerConnection(infraConfig, control);
let mediaStream;
try {
mediaStream =
await navigator.mediaDevices.getUserMedia({video: true, audio: true});
const tracks = mediaStream.getTracks();
tracks.forEach(track => {
console.log(`Using ${track.kind} device: ${track.label}`);
pc.addTrack(track, mediaStream);
});
} catch (e) {
console.error("Failed to open audio device: ", e);
}
let deviceConnection = new DeviceConnection(pc, control, mediaStream);
deviceConnection.description = deviceInfo;
async function acceptOfferAndReplyAnswer(offer) {
try {
await pc.setRemoteDescription(offer);
let answer = await pc.createAnswer();
console.log('Answer: ', answer);
await pc.setLocalDescription(answer);
await control.sendClientDescription(answer);
} catch (e) {
console.error('Error establishing WebRTC connection: ', e)
throw e;
}
}
control.onOffer(desc => {
console.log('Offer: ', desc);
acceptOfferAndReplyAnswer(desc);
});
control.onIceCandidate(iceCandidate => {
console.log(`Remote ICE Candidate: `, iceCandidate);
pc.addIceCandidate(iceCandidate);
});
pc.addEventListener('icecandidate', evt => {
if (evt.candidate) control.sendIceCandidate(evt.candidate);
});
let connected_promise = new Promise((resolve, reject) => {
pc.addEventListener('connectionstatechange', evt => {
let state = pc.connectionState;
if (state == 'connected') {
resolve(deviceConnection);
} else if (state == 'failed') {
reject(evt);
}
});
});
control.ConnectDevice();
return connected_promise;
}