Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Open sidebar
LINAGORA
L
LGS
Labs
hublot
Commits
df47a134
Commit
df47a134
authored
Nov 14, 2017
by
Tom JORQUERA
Browse files
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
Changes
13
Pipelines
2
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
704 additions
and
632 deletions
+704
-632
Dockerfile
Dockerfile
+11
-1
client/controller/controller.js
client/controller/controller.js
+2
-4
client/controller/controller.test.js
client/controller/controller.test.js
+3
-0
client/lib/speech-to-text.js
client/lib/speech-to-text.js
+38
-33
client/robot/robot.js
client/robot/robot.js
+12
-13
client/robot/robot.test.js
client/robot/robot.test.js
+3
-1
config.json
config.json
+12
-17
docker-compose.yml
docker-compose.yml
+6
-10
lib/controller.js
lib/controller.js
+25
-28
lib/controller.test.js
lib/controller.test.js
+43
-49
lib/runner.js
lib/runner.js
+113
-36
package.json
package.json
+1
-1
yarn.lock
yarn.lock
+435
-439
No files found.
Dockerfile
View file @
df47a134
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
...
...
client/controller/controller.js
View file @
df47a134
...
...
@@ -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
++
)
{
...
...
client/controller/controller.test.js
View file @
df47a134
...
...
@@ -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 */
...
...
client/lib/speech-to-text.js
View file @
df47a134
...
...
@@ -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
;
}
}
};
};
client/robot/robot.js
View file @
df47a134
...
...
@@ -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
;
}
};
...
...
client/robot/robot.test.js
View file @
df47a134
...
...
@@ -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 */
});
...
...
config.json
View file @
df47a134
...
...
@@ -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"
:
{
...
...
docker-compose.yml
View file @
df47a134
...
...
@@ -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
lib/controller.js
View file @
df47a134
...
...
@@ -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
;
};
...
...
lib/controller.test.js
View file @
df47a134
...
...
@@ -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
'
);
});
});
lib/runner.js
View file @
df47a134
...
...
@@ -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
'
);
// L