Commit df47a134 authored by Tom JORQUERA's avatar Tom JORQUERA

Merge branch 'puppeteer' into 'master'

Replace selenium by puppeteer

Closes #64 and #65

See merge request !52
parents fed80bf1 5a58aab1
Pipeline #5336 passed with stage
in 2 minutes and 27 seconds
FROM node:8-alpine
FROM node:8-slim
RUN apt-get update && apt-get install -y wget --no-install-recommends \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-unstable \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get purge --auto-remove -y curl \
&& rm -rf /src/*.deb
WORKDIR /usr/src/app/hublot
......
......@@ -23,11 +23,9 @@
//
// It is the first file of the `controller` folder to be loaded in the client.
/* global robotController:true document angular easyrtc */
/* global robotController:true room document angular easyrtc */
/* exported robotController */
const room = arguments[0];
robotController = {
$scope: angular.element(document.body).scope().$root,
$injector: angular.element(document.body).injector(),
......@@ -45,7 +43,7 @@ robotController = {
},
getRemoteParticipants: () => {
const participants = easyrtc.getRoomOccupantsAsArray(room);
const participants = easyrtc.getRoomOccupantsAsArray(room); // Note that room is exposed globally
const res = [];
if (participants) {
for (let i = 0; i < participants.length; i++) {
......
......@@ -69,6 +69,9 @@ describe('client/controller', () => {
global.angular = angular;
global.easyrtc = easyRTCMock(['p1', 'p2', 'p3']);
// Room is supposed to be exposed globally
global.room = 'test';
/* eslint-disable import/no-unassigned-import */
require('./controller.js');
/* eslint-enable */
......
......@@ -25,41 +25,46 @@
robotLib.stt = function (config) {
return {
getTranscriptSocket: onSegment => {
const ws = new WebSocket(config.gstreamerURL + '/client/ws/speech?content-type=audio/x-matroska,,+rate=(int)48000,+channels=(int)1');
ws.onopen = function () {
console.info('ws to stt module open');
};
ws.onclose = function () {
console.info('ws to stt module closed');
};
ws.onerror = function (event) {
console.info('ws to stt module error: ' + event);
};
ws.onmessage = function (event) {
const hyp = JSON.parse(event.data);
if (hyp.status === 0) {
if (hyp.result !== undefined && hyp.result.final) {
const trans = ((hyp.result.hypotheses)[0]).transcript;
let start;
let end;
if (hyp['segment-start'] && hyp['segment-length']) {
start = JSON.parse(hyp['segment-start']);
end = parseFloat(hyp['segment-start']) + parseFloat(hyp['segment-length']);
} else {
const time = new Date().getTime() / 1000;
start = time;
end = time + 1;
}
try {
const ws = new WebSocket(config.gstreamerURL + '/client/ws/speech?content-type=audio/x-matroska,,+rate=(int)48000,+channels=(int)1');
ws.onopen = function () {
console.info('ws to stt module open');
};
ws.onclose = function () {
console.info('ws to stt module closed');
};
ws.onerror = function (event) {
console.info('ws to stt module error: ' + event);
};
ws.onmessage = function (event) {
const hyp = JSON.parse(event.data);
if (hyp.status === 0) {
if (hyp.result !== undefined && hyp.result.final) {
const trans = ((hyp.result.hypotheses)[0]).transcript;
let start;
let end;
if (hyp['segment-start'] && hyp['segment-length']) {
start = JSON.parse(hyp['segment-start']);
end = parseFloat(hyp['segment-start']) + parseFloat(hyp['segment-length']);
} else {
const time = new Date().getTime() / 1000;
start = time;
end = time + 1;
}
onSegment({
from: start,
until: end,
text: trans
});
onSegment({
from: start,
until: end,
text: trans
});
}
}
}
};
return ws;
};
return ws;
} catch (err) {
console.error(err);
return null;
}
}
};
};
......@@ -119,8 +119,12 @@ robot = {
},
stopRecordParticipant(easyrtcid) {
robot.participantsMediaRecorders[easyrtcid].stop();
robot.recordedParticipantsWS[easyrtcid].close();
if (robot.participantsMediaRecorders[easyrtcid]) {
robot.participantsMediaRecorders[easyrtcid].stop();
}
if (robot.recordedParticipantsWS[easyrtcid]) {
robot.recordedParticipantsWS[easyrtcid].close();
}
},
checkDisconnect() {
......@@ -156,9 +160,7 @@ robot = {
robotLib.archive = robotLib.archive(robot.clientConfig);
robotController.onAttendeePush = (e, data) => {
if (data.easyrtcid === robotController.getMyId()) {
robot.onConnection();
} else {
if (data.easyrtcid !== robotController.getMyId()) {
robot.recordParticipant(data.easyrtcid);
}
};
......@@ -171,9 +173,7 @@ robot = {
robotController.getWebRTCAdapter().addDisconnectCallback(() => {
robot.clearConnection();
});
},
onConnection: () => {
const recoStartRetry = () => {
if (robotLib.reco.start(robot.room)) {
clearInterval(robot.recoInterval);
......@@ -204,14 +204,13 @@ robot = {
robot.isDisconnected = true;
robotController.disconnect();
robot.clearConnection();
robot.notifyEndToServer();
return true;
},
// This function is set by the server
/* eslint-disable no-undef */
notifyEndToServer();
/* eslint-enable */
// This function will be overridden by a server callback
notifyEndToServer: () => {
console.error('`notifyEndToServer`: Server has not registered a callback!');
return true;
}
};
......
......@@ -82,6 +82,9 @@ describe('client/robot without init', () => {
})
};
// This is supposed to be set by the server
global.notifyEndToServer = () => {};
/* eslint-disable import/no-unassigned-import */
require('./robot.js');
/* eslint-enable */
......@@ -184,7 +187,6 @@ describe('client/robot', () => {
/* eslint-disable import/no-unassigned-import */
require('./robot.js');
global.robot.start();
global.robot.onConnection();
/* eslint-enable */
});
......
......@@ -3,23 +3,18 @@
"url": "http://hubl.in"
},
"runner": {
"driver" : {
"host": "selenium",
"port": 4444,
"desiredCapabilities": {
"browserName": "chrome",
"chromeOptions": {
"args" : [
"--disable-web-security",
"--user-data-dir=./tmp/chromium",
"--allow-running-insecure-content",
"--use-fake-device-for-media-stream",
"--use-file-for-fake-audio-capture=/opt/media/silence.wav",
"--use-file-for-fake-video-capture=/opt/media/logo.y4m",
"--use-fake-ui-for-media-stream"
]
}
}
"displayClientConsole": false,
"puppeteer": {
"headless": true,
"args" : [
"--disable-web-security",
"--user-data-dir=./tmp/chromium",
"--allow-running-insecure-content",
"--use-fake-device-for-media-stream",
"--use-file-for-fake-audio-capture=/opt/media/silence.wav",
"--use-file-for-fake-video-capture=/opt/media/logo.y4m",
"--use-fake-ui-for-media-stream"
]
}
},
"client": {
......
......@@ -8,16 +8,19 @@ services:
depends_on:
- recommender
- kaldi-gstreamer
- selenium
volumes:
- ./media:/opt/media
ports:
- "3000:3000"
recommender:
image: linagora/recommender
ports:
expose:
- "8080"
kaldi-gstreamer:
image: linagora/kaldi-gstreamer
ports:
expose:
- "80"
volumes:
- ${MODELS_PATH}:/opt/models
......@@ -26,10 +29,3 @@ services:
- NB_WORKERS
- YAML
- MODELS_PATH
selenium:
image: selenium/standalone-chrome
ports:
- "4444"
volumes:
- ./media:/opt/media
......@@ -26,50 +26,47 @@
const create = (runner, modules, config) => {
const controller = {
registry: {},
client: room => {
client: async room => {
if (room in controller.registry) {
return null;
}
controller.registry[room] = runner.run(modules,
config.visio.url,
room,
config.client);
controller.registry[room] = await runner.run(modules,
config.visio.url,
room,
config.client);
// Add callback for client to inform when it leaves the room
controller.registry[room]
.timeouts('script', 2147483000) // We need big timeouts here (here ~600h)
.executeAsync(done => {
// Note that this code is executed in the selenium-driven browser
/* eslint-disable no-undef */
robot.notifyEndToServer = () => {
done('finished');
console.log('notified server I finished');
};
/* eslint-enable */
}).then(() => {
delete controller.registry[room];
console.log('client for room', room, 'finished');
}).catch(err => {
console.log('got error', err);
// Careful, there is a bug here with chomedriver; if we open the
// console of the selenium-driven browser, an exception is thrown.
// This means that this part may break in dev (but not in prod).
// See issue #65 for more details.
});
registerClientEndCallback(room);
return controller.registry[room];
},
forceDisconnect: room => {
controller.registry[room].execute(() => {
if (!controller.registry[room]) {
return false;
}
controller.registry[room].evaluate(() => {
/* eslint-disable no-undef */
robot.stop();
/* eslint-enable */
}).end();
delete controller.registry[room];
});
// Client-side `robot.stop` will call `registerClientEndCallback`
// callback to clean registry
return true;
}
};
function registerClientEndCallback(room) {
controller.registry[room].exposeFunction(
'notifyEndToServer',
() => {
controller.registry[room].close();
delete controller.registry[room];
console.log('client for room', room, 'finished');
});
}
return controller;
};
......
......@@ -24,34 +24,31 @@ const {create} = require('./controller.js');
// Here are some needed mocks
const functionRunnerMock = {
callbacks: [],
catch() {
return functionRunnerMock;
},
end() {
return functionRunnerMock;
},
execute() {
return functionRunnerMock;
},
executeAsync() {
return functionRunnerMock;
},
timeouts() {
return functionRunnerMock;
},
then(callback) {
functionRunnerMock.callbacks.push(callback);
return functionRunnerMock;
global.robot = {
stopCalled: false,
stop: () => {
global.robot.stopCalled = true;
}
};
function mockClient() {
const res = {
exposedFunctions: {},
exposeFunction: (name, f) => {
res.exposedFunctions[name] = f;
},
evaluate: f => {
f();
},
close: () => {}
};
return res;
}
const runnerMock = {
run: () => {
return functionRunnerMock;
exposedFunction: {},
run: async () => {
return mockClient();
}
};
......@@ -64,53 +61,50 @@ let controller;
describe('controller', () => {
beforeEach(() => {
global.robot.stopCalled = false;
controller = create(runnerMock, [], configMock);
functionRunnerMock.callbacks = [];
});
test('should allow to create a client to a new room', () => {
expect(controller.client).toBeDefined();
});
test('should register a newly created client to its room', () => {
const client = controller.client('test');
test('should register a newly created client to its room', async done => {
const client = await controller.client('test');
expect(controller.registry).toHaveProperty('test', client);
done();
});
test('should return null when trying to create a client for an existing room', () => {
controller.client('test');
const client2 = controller.client('test');
test('should return null when trying to create a client for an existing room', async done => {
await controller.client('test');
const client2 = await controller.client('test');
expect(client2).toBeNull();
done();
});
test('should not replace the client for an existing room', () => {
const client1 = controller.client('test');
controller.client('test');
test('should not replace the client for an existing room', async () => {
const client1 = await controller.client('test');
await controller.client('test');
expect(controller.registry).toHaveProperty('test', client1);
});
test('should not have any room in registry when the client is disconnect', () => {
controller.client('test');
test('should execute robot.stop() when the client is disconnect', async () => {
await controller.client('test');
expect(global.robot.stopCalled).toBe(false);
controller.forceDisconnect('test');
expect(controller.registry).toEqual({});
});
test('should have a room in registry when one client disconnect', () => {
controller.client('test1');
const client2 = controller.client('test2');
controller.forceDisconnect('test1');
expect(controller.registry).toHaveProperty('test2', client2);
expect(global.robot.stopCalled).toBe(true);
});
test('should have registered a finish callback to the client', () => {
controller.client('test');
expect(functionRunnerMock.callbacks.length).toBe(1);
test('should have registered a finish callback to the client', async () => {
const client = await controller.client('test');
expect(client.exposedFunctions).toHaveProperty('notifyEndToServer');
expect(client.exposedFunctions.notifyEndToServer).not.toBeNull();
});
test('should clean registry on client finish callback', () => {
controller.client('test');
test('should clean registry on client finish callback', async () => {
const client = await controller.client('test');
expect(controller.registry).toHaveProperty('test');
functionRunnerMock.callbacks[0]();
client.exposedFunctions.notifyEndToServer();
expect(controller.registry).not.toHaveProperty('test');
});
});
......@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const webdriverio = require('webdriverio');
const puppeteer = require('puppeteer');
// Utility function to resolve a list of promise-based function calls
// on a given element list sequentially
......@@ -28,39 +28,116 @@ function resolveSequentially(f, elements) {
new Promise(resolve => resolve()));
}
module.exports = config => ({
run: (controllerFilesList, server, room, clientConfig) => {
console.log('runner is up');
const client = webdriverio.remote(config.driver);
console.log('runner created client');
return client.init()
.then(() => console.log('runner: client started'))
.url(server + '/' + room)
.then(() => console.log('runner: connecting to url'))
.waitForVisible('#displayname', 30000)
.then(() => console.log('runner: displayname visible'))
.then(() => resolveSequentially(f => client.execute(f, room, clientConfig),
controllerFilesList))
.then(() => console.log('runner: modules resolved'))
.then(() => client.execute(clientConfig =>
/* eslint-disable no-undef */
robotController.external.load(clientConfig), clientConfig)
/* eslint-enable */
)
.then(() => console.log('runner: external loaded'))
.then(() => client.execute((room, clientConfig) => {
setTimeout(() => {
/* eslint-disable no-undef */
robot.start(room, clientConfig);
/* eslint-enable */
}, 500);
}, room, clientConfig))
.then(() => console.log('runner: robot started'))
.setValue('#displayname', clientConfig.name)
.click('.btn')
.then(() => console.log('runner: button clicked'))
.waitForExist('//div[@video-id="video-thumb8"]', 30000)
.then(() => console.log('runner: video exists'))
.catch(err => console.log('runner: error %j', err));
module.exports = config => {
const b = puppeteer.launch(config.puppeteer);
function displayClientConsole(page, room) {
page.on('console', msg => {
if (msg.type === 'error') {
console.log('room', room, ':', '[' + msg.type + ']', msg.args[0].jsonValue());
} else {
console.log('room', room, ':', '[' + msg.type + ']', msg.text);
}
});
page.on('warning', warn => {
console.error('room', room, ':', '[WARN ' + warn.code + ']', warn.message);
console.error('room', room, ':', warn.stack);
});
page.on('error', err => {
console.error('room', room, ':', '[ERROR ' + err.code + ']', err.message);
console.error('room', room, ':', err.stack);
});
page.on('pageerror', err => {
console.error('room', room, ':', '[PAGEERROR]', err);
});
}
});
return {
run: async (controllerFilesList, server, room, clientConfig) => {
try {
const browser = await b;
console.log('runner is up');
const page = await browser.newPage();
console.log('runner: client started');
if (config.displayClientConsole) {
displayClientConsole(page, room);
}
// Connect to the room
await page.goto(server + '/' + room);
console.log('runner: connecting to url');
await page.waitForSelector('#displayname', {
visible: true
});
console.log('runner: displayname visible');
const nameField = await page.$('#displayname');
// Clean field of possible old input
await nameField.focus();
await page.keyboard.down('Control');
await page.keyboard.press('a');
await page.keyboard.up('Control');
await page.keyboard.press('Delete');
await nameField.type(clientConfig.name);
const submitButton = await page.$('.btn');
await submitButton.click();
console.log('runner: name submitted');
await page.waitForSelector('[video-id=video-thumb8]');
console.log('runner: video exists');
// Expose the room globally
await page.evaluate(
r => {
room = r;
},
room);
console.log('runner: room set');
// Load the robot
await resolveSequentially(
source => page.evaluate(source),
controllerFilesList);
console.log('runner: modules resolved');
await page.evaluate(
clientConfig => {
/* eslint-disable no-undef */
robotController.external.load(clientConfig);
/* eslint-enable */
},
clientConfig);
console.log('runner: external loaded');
// Run the robot
await page.evaluate((room, clientConfig) => {
setTimeout(
() => {
/* eslint-disable no-undef */
robot.start(room, clientConfig);
/* eslint-enable */
},
500);
}, room, clientConfig);
console.log('runner: robot started');
return page;
} catch (err) {
console.error('runner: error ', err);
return null;
}
}
};
};
......@@ -16,7 +16,7 @@
"body-parser": "^1.18.2",
"express": "^4.16.2",
"mz": "2.6.0",
"webdriverio": "4.7.1"
"puppeteer": "0.12.0"
},
"devDependencies": {
"jest": "20.0.0",
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment