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 WORKDIR /usr/src/app/hublot
......
...@@ -23,11 +23,9 @@ ...@@ -23,11 +23,9 @@
// //
// It is the first file of the `controller` folder to be loaded in the client. // 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 */ /* exported robotController */
const room = arguments[0];
robotController = { robotController = {
$scope: angular.element(document.body).scope().$root, $scope: angular.element(document.body).scope().$root,
$injector: angular.element(document.body).injector(), $injector: angular.element(document.body).injector(),
...@@ -45,7 +43,7 @@ robotController = { ...@@ -45,7 +43,7 @@ robotController = {
}, },
getRemoteParticipants: () => { getRemoteParticipants: () => {
const participants = easyrtc.getRoomOccupantsAsArray(room); const participants = easyrtc.getRoomOccupantsAsArray(room); // Note that room is exposed globally
const res = []; const res = [];
if (participants) { if (participants) {
for (let i = 0; i < participants.length; i++) { for (let i = 0; i < participants.length; i++) {
......
...@@ -69,6 +69,9 @@ describe('client/controller', () => { ...@@ -69,6 +69,9 @@ describe('client/controller', () => {
global.angular = angular; global.angular = angular;
global.easyrtc = easyRTCMock(['p1', 'p2', 'p3']); global.easyrtc = easyRTCMock(['p1', 'p2', 'p3']);
// Room is supposed to be exposed globally
global.room = 'test';
/* eslint-disable import/no-unassigned-import */ /* eslint-disable import/no-unassigned-import */
require('./controller.js'); require('./controller.js');
/* eslint-enable */ /* eslint-enable */
......
...@@ -25,41 +25,46 @@ ...@@ -25,41 +25,46 @@
robotLib.stt = function (config) { robotLib.stt = function (config) {
return { return {
getTranscriptSocket: onSegment => { getTranscriptSocket: onSegment => {
const ws = new WebSocket(config.gstreamerURL + '/client/ws/speech?content-type=audio/x-matroska,,+rate=(int)48000,+channels=(int)1'); try {
ws.onopen = function () { const ws = new WebSocket(config.gstreamerURL + '/client/ws/speech?content-type=audio/x-matroska,,+rate=(int)48000,+channels=(int)1');
console.info('ws to stt module open'); ws.onopen = function () {
}; console.info('ws to stt module open');
ws.onclose = function () { };
console.info('ws to stt module closed'); ws.onclose = function () {
}; console.info('ws to stt module closed');
ws.onerror = function (event) { };
console.info('ws to stt module error: ' + event); ws.onerror = function (event) {
}; console.info('ws to stt module error: ' + event);
ws.onmessage = function (event) { };
const hyp = JSON.parse(event.data); ws.onmessage = function (event) {
if (hyp.status === 0) { const hyp = JSON.parse(event.data);
if (hyp.result !== undefined && hyp.result.final) { if (hyp.status === 0) {
const trans = ((hyp.result.hypotheses)[0]).transcript; if (hyp.result !== undefined && hyp.result.final) {
let start; const trans = ((hyp.result.hypotheses)[0]).transcript;
let end; let start;
if (hyp['segment-start'] && hyp['segment-length']) { let end;
start = JSON.parse(hyp['segment-start']); if (hyp['segment-start'] && hyp['segment-length']) {
end = parseFloat(hyp['segment-start']) + parseFloat(hyp['segment-length']); start = JSON.parse(hyp['segment-start']);
} else { end = parseFloat(hyp['segment-start']) + parseFloat(hyp['segment-length']);
const time = new Date().getTime() / 1000; } else {
start = time; const time = new Date().getTime() / 1000;
end = time + 1; start = time;
} end = time + 1;
}
onSegment({ onSegment({
from: start, from: start,
until: end, until: end,
text: trans text: trans
}); });
}
} }
} };
}; return ws;
return ws; } catch (err) {
console.error(err);
return null;
}
} }
}; };
}; };
...@@ -119,8 +119,12 @@ robot = { ...@@ -119,8 +119,12 @@ robot = {
}, },
stopRecordParticipant(easyrtcid) { stopRecordParticipant(easyrtcid) {
robot.participantsMediaRecorders[easyrtcid].stop(); if (robot.participantsMediaRecorders[easyrtcid]) {
robot.recordedParticipantsWS[easyrtcid].close(); robot.participantsMediaRecorders[easyrtcid].stop();
}
if (robot.recordedParticipantsWS[easyrtcid]) {
robot.recordedParticipantsWS[easyrtcid].close();
}
}, },
checkDisconnect() { checkDisconnect() {
...@@ -156,9 +160,7 @@ robot = { ...@@ -156,9 +160,7 @@ robot = {
robotLib.archive = robotLib.archive(robot.clientConfig); robotLib.archive = robotLib.archive(robot.clientConfig);
robotController.onAttendeePush = (e, data) => { robotController.onAttendeePush = (e, data) => {
if (data.easyrtcid === robotController.getMyId()) { if (data.easyrtcid !== robotController.getMyId()) {
robot.onConnection();
} else {
robot.recordParticipant(data.easyrtcid); robot.recordParticipant(data.easyrtcid);
} }
}; };
...@@ -171,9 +173,7 @@ robot = { ...@@ -171,9 +173,7 @@ robot = {
robotController.getWebRTCAdapter().addDisconnectCallback(() => { robotController.getWebRTCAdapter().addDisconnectCallback(() => {
robot.clearConnection(); robot.clearConnection();
}); });
},
onConnection: () => {
const recoStartRetry = () => { const recoStartRetry = () => {
if (robotLib.reco.start(robot.room)) { if (robotLib.reco.start(robot.room)) {
clearInterval(robot.recoInterval); clearInterval(robot.recoInterval);
...@@ -204,14 +204,13 @@ robot = { ...@@ -204,14 +204,13 @@ robot = {
robot.isDisconnected = true; robot.isDisconnected = true;
robotController.disconnect(); robotController.disconnect();
robot.clearConnection(); 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 return true;
notifyEndToServer: () => {
console.error('`notifyEndToServer`: Server has not registered a callback!');
} }
}; };
......
...@@ -82,6 +82,9 @@ describe('client/robot without init', () => { ...@@ -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 */ /* eslint-disable import/no-unassigned-import */
require('./robot.js'); require('./robot.js');
/* eslint-enable */ /* eslint-enable */
...@@ -184,7 +187,6 @@ describe('client/robot', () => { ...@@ -184,7 +187,6 @@ describe('client/robot', () => {
/* eslint-disable import/no-unassigned-import */ /* eslint-disable import/no-unassigned-import */
require('./robot.js'); require('./robot.js');
global.robot.start(); global.robot.start();
global.robot.onConnection();
/* eslint-enable */ /* eslint-enable */
}); });
......
...@@ -3,23 +3,18 @@ ...@@ -3,23 +3,18 @@
"url": "http://hubl.in" "url": "http://hubl.in"
}, },
"runner": { "runner": {
"driver" : { "displayClientConsole": false,
"host": "selenium", "puppeteer": {
"port": 4444, "headless": true,
"desiredCapabilities": { "args" : [
"browserName": "chrome", "--disable-web-security",
"chromeOptions": { "--user-data-dir=./tmp/chromium",
"args" : [ "--allow-running-insecure-content",
"--disable-web-security", "--use-fake-device-for-media-stream",
"--user-data-dir=./tmp/chromium", "--use-file-for-fake-audio-capture=/opt/media/silence.wav",
"--allow-running-insecure-content", "--use-file-for-fake-video-capture=/opt/media/logo.y4m",
"--use-fake-device-for-media-stream", "--use-fake-ui-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": { "client": {
......
...@@ -8,16 +8,19 @@ services: ...@@ -8,16 +8,19 @@ services:
depends_on: depends_on:
- recommender - recommender
- kaldi-gstreamer - kaldi-gstreamer
- selenium volumes:
- ./media:/opt/media
ports:
- "3000:3000"
recommender: recommender:
image: linagora/recommender image: linagora/recommender
ports: expose:
- "8080" - "8080"
kaldi-gstreamer: kaldi-gstreamer:
image: linagora/kaldi-gstreamer image: linagora/kaldi-gstreamer
ports: expose:
- "80" - "80"
volumes: volumes:
- ${MODELS_PATH}:/opt/models - ${MODELS_PATH}:/opt/models
...@@ -26,10 +29,3 @@ services: ...@@ -26,10 +29,3 @@ services:
- NB_WORKERS - NB_WORKERS
- YAML - YAML
- MODELS_PATH - MODELS_PATH
selenium:
image: selenium/standalone-chrome
ports:
- "4444"
volumes:
- ./media:/opt/media
...@@ -26,50 +26,47 @@ ...@@ -26,50 +26,47 @@
const create = (runner, modules, config) => { const create = (runner, modules, config) => {
const controller = { const controller = {
registry: {}, registry: {},
client: room => { client: async room => {
if (room in controller.registry) { if (room in controller.registry) {
return null; return null;
} }
controller.registry[room] = runner.run(modules, controller.registry[room] = await runner.run(modules,
config.visio.url, config.visio.url,
room, room,
config.client); config.client);
// Add callback for client to inform when it leaves the room // Add callback for client to inform when it leaves the room
controller.registry[room] registerClientEndCallback(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.
});
return controller.registry[room]; return controller.registry[room];
}, },
forceDisconnect: room => { forceDisconnect: room => {
controller.registry[room].execute(() => { if (!controller.registry[room]) {
return false;
}
controller.registry[room].evaluate(() => {
/* eslint-disable no-undef */ /* eslint-disable no-undef */
robot.stop(); robot.stop();
/* eslint-enable */ /* 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; return controller;
}; };
......
...@@ -24,34 +24,31 @@ const {create} = require('./controller.js'); ...@@ -24,34 +24,31 @@ const {create} = require('./controller.js');
// Here are some needed mocks // Here are some needed mocks
const functionRunnerMock = { global.robot = {
stopCalled: false,
callbacks: [], stop: () => {
global.robot.stopCalled = true;
catch() {
return functionRunnerMock;
},
end() {
return functionRunnerMock;
},
execute() {
return functionRunnerMock;
},
executeAsync() {
return functionRunnerMock;
},
timeouts() {
return functionRunnerMock;
},
then(callback) {
functionRunnerMock.callbacks.push(callback);
return functionRunnerMock;
} }
}; };
function mockClient() {
const res = {
exposedFunctions: {},
exposeFunction: (name, f) => {
res.exposedFunctions[name] = f;
},
evaluate: f => {
f();
},
close: () => {}
};
return res;
}
const runnerMock = { const runnerMock = {
run: () => { exposedFunction: {},
return functionRunnerMock; run: async () => {
return mockClient();
} }
}; };
...@@ -64,53 +61,50 @@ let controller; ...@@ -64,53 +61,50 @@ let controller;
describe('controller', () => { describe('controller', () => {
beforeEach(() => { beforeEach(() => {
global.robot.stopCalled = false;
controller = create(runnerMock, [], configMock); controller = create(runnerMock, [], configMock);
functionRunnerMock.callbacks = [];
}); });
test('should allow to create a client to a new room', () => { test('should allow to create a client to a new room', () => {
expect(controller.client).toBeDefined(); expect(controller.client).toBeDefined();
}); });
test('should register a newly created client to its room', () => { test('should register a newly created client to its room', async done => {
const client = controller.client('test'); const client = await controller.client('test');
expect(controller.registry).toHaveProperty('test', client); expect(controller.registry).toHaveProperty('test', client);
done();
}); });
test('should return null when trying to create a client for an existing room', () => { test('should return null when trying to create a client for an existing room', async done => {
controller.client('test'); await controller.client('test');
const client2 = controller.client('test'); const client2 = await controller.client('test');
expect(client2).toBeNull(); expect(client2).toBeNull();
done();
}); });
test('should not replace the client for an existing room', () => { test('should not replace the client for an existing room', async () => {
const client1 = controller.client('test'); const client1 = await controller.client('test');
controller.client('test'); await controller.client('test');
expect(controller.registry).toHaveProperty('test', client1); expect(controller.registry).toHaveProperty('test', client1);
}); });
test('should not have any room in registry when the client is disconnect', () => { test('should execute robot.stop() when the client is disconnect', async () => {
controller.client('test'); await controller.client('test');
expect(global.robot.stopCalled).toBe(false);
controller.forceDisconnect('test'); controller.forceDisconnect('test');
expect(controller.registry).toEqual({}); expect(global.robot.stopCalled).toBe(true);
});
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);
}); });
test('should have registered a finish callback to the client', () => { test('should have registered a finish callback to the client', async () => {
controller.client('test'); const client = await controller.client('test');
expect(functionRunnerMock.callbacks.length).toBe(1); expect(client.exposedFunctions).toHaveProperty('notifyEndToServer');
expect(client.exposedFunctions.notifyEndToServer).not.toBeNull();
}); });
test('should clean registry on client finish callback', () => { test('should clean registry on client finish callback', async () => {
controller.client('test'); const client = await controller.client('test');
expect(controller.registry).toHaveProperty('test'); expect(controller.registry).toHaveProperty('test');
functionRunnerMock.callbacks[0](); client.exposedFunctions.notifyEndToServer();
expect(controller.registry).not.toHaveProperty('test'); expect(controller.registry).not.toHaveProperty('test');
}); });
}); });
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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 // Utility function to resolve a list of promise-based function calls
// on a given element list sequentially // on a given element list sequentially
...@@ -28,39 +28,116 @@ function resolveSequentially(f, elements) { ...@@ -28,39 +28,116 @@ function resolveSequentially(f, elements) {
new Promise(resolve => resolve())); new Promise(resolve => resolve()));
} }
module.exports = config => ({ module.exports = config => {
run: (controllerFilesList, server, room, clientConfig) => { const b = puppeteer.launch(config.puppeteer);
console.log('runner is up');
const client = webdriverio.remote(config.driver); function displayClientConsole(page, room) {
console.log('runner created client'); page.on('console', msg => {
return client.init() if (msg.type === 'error') {
.then(() => console.log('runner: client started')) console.log('room', room, ':', '[' + msg.type + ']', msg.args[0].jsonValue());
.url(server + '/' + room) } else {
.then(() => console.log('runner: connecting to url')) console.log('room', room, ':', '[' + msg.type + ']', msg.text);
.waitForVisible('#displayname', 30000) }
.then(() => console.log('runner: displayname visible')) });
.then(() => resolveSequentially(f => client.execute(f, room, clientConfig),
controllerFilesList)) page.on('warning', warn => {
.then(() => console.log('runner: modules resolved')) console.error('room', room, ':', '[WARN ' + warn.code + ']', warn.message);
.then(() => client.execute(clientConfig => console.error('room', room, ':', warn.stack);
/* eslint-disable no-undef */ });
robotController.external.load(clientConfig), clientConfig)
/* eslint-enable */ page.on('error', err => {
) console.error('room', room, ':', '[ERROR ' + err.code + ']', err.message);
.then(() => console.log('runner: external loaded')) console.error('room', room, ':', err.stack);
.then(() => client.execute((room, clientConfig) => { });
setTimeout(() => {
/* eslint-disable no-undef */ page.on('pageerror', err => {
robot.start(room, clientConfig); console.error('room', room, ':', '[PAGEERROR]', err);
/* 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));
} }
});
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(