Commit 33e789b9 authored by Clifford Cheng's avatar Clifford Cheng Committed by Commit Bot

Move Media Router telemetry tests to the correct folder under tools/perf.

The dialog tests are all removed because Media Router is switching to a new UI.
There are also a number of code refactoring because of this change.

Change-Id: I9bede536041e104d727fadac81089e47013711d9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1437273
Commit-Queue: mark a. foltz <mfoltz@chromium.org>
Reviewed-by: default avatarCharlie Andrews <charliea@chromium.org>
Reviewed-by: default avatarmark a. foltz <mfoltz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#642671}
parent 72bdcc1c
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
copy("media_router_telemetry_extension") {
sources = [
"extension/manifest.json",
"extension/script.js",
]
outputs = [
"$root_out_dir/media_router/media_router_telemetry_extension/{{source_file_part}}",
]
}
cliffordcheng@chromium.org
leilei@chromium.org
This is the test extension which is used to get CPU/memory usage for Media
Router performance test.
`chrome/test/media_router/telemetry/benchmarks/pagesets/cpu_memory_script.js`
shows how to interact with this test extension to get CPU/memory usage.
{
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1nP6oOqlQh79uPMoPJQT1aj7z1vM4UkQ3y8rhgnH56FApNWIIr8NqRqSBc1WeCXskdQjctvjW/1ZSYmYAplLziEjlFED2yfCATRl8V5spsZjCqtBw4O8IjzKkEhsNYBlBrpOu0jSLP4Nr/enJvzLwe4tQ80XuLSItJzxF72SoPbxCQCCbm3NKe5dyqZYKmroiLDjwDa8jz3FM6uohfJaWVFzp+fSK1CiKPRxlGvp0Oy5LM03vJFDGwdwJM6nXdy3eM4tIH5arbJ1EfPs5M2AKPNPo55wOLhFjyZzo1TBj9wP1V/r6WmR4Q8XVvpWTIVcRH5VxeOTWoFKYRGQ6OICQIDAQAB",
"description": "This extension helps in retrieving the CPU/memory usage stats for MR extension.",
"name": "MR Test Extension",
"version": "1.0",
"manifest_version": 2,
"externally_connectable": {
"matches": [
"http://127.0.0.1:*/*"
]
},
"background": {
"scripts": ["script.js"],
"persistent": true
},
"permissions": [
"processes"
]
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Extension ID: ocihafebdemjoofhbdnamghkfobfgbal
// Collects CPU/memory usage information and posts to the page.
function collectData(port) {
function processCpuListener(processes) {
_postData(processes, port, 'cpu');
}
function processMemoryListener(processes) {
_postData(processes, port, 'privateMemory');
}
chrome.processes.onUpdated.addListener(processCpuListener);
chrome.processes.onUpdatedWithMemory.addListener(processMemoryListener);
port.onDisconnect.addListener(function() {
chrome.processes.onUpdated.removeListener(processCpuListener);
chrome.processes.onUpdated.removeListener(processMemoryListener);
});
}
/**
* Posts the metric data to the page.
*
* @param processes list of current processes.
* @param port the port used for the communication between the page and
* extension.
* @param metric_name the metric name, e.g cpu.
*/
function _postData(processes, port, metric_name) {
let tabPid = port.sender.tab.id;
if (!tabPid) {
return;
}
let tabProcess = "";
for (let p in processes) {
for (let task in processes[p].tasks) {
if (processes[p].tasks[task].tabId == tabPid)
tabProcess = processes[p].osProcessId;
}
if (tabProcess)
break;
}
if (!tabProcess) {
return;
}
let message = {};
message[metric_name] = {'current_tab': tabProcess[metric_name]};
for (let pid in processes) {
let process = processes[pid];
data = process[metric_name];
if (['browser', 'gpu', 'extension'].indexOf(process.type) > -1) {
if (process.type == 'extension'){
for (let index in process.tasks) {
let task = process.tasks[index];
if (task.title && task.title.indexOf('Chrome Media Router') > -1) {
message[metric_name]['mr_' + process.type] = data;
}
}
} else {
message[metric_name][process.type] = data;
}
}
}
port.postMessage(message);
}
chrome.runtime.onConnectExternal.addListener(function(port) {
if (port.name == 'collectData') {
collectData(port);
} else {
console.warn('Unknown port, disconnect the port.');
port.disconnect();
}
});
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
import time
from telemetry import page
from telemetry.core import exceptions
class MediaRouterBasePage(page.Page):
"""Abstract Cast page for Media Router Telemetry tests."""
def ChooseSink(self, tab, sink_name):
"""Chooses a specific sink in the list."""
tab.ExecuteJavaScript("""
var sinks = window.document.getElementById("media-router-container").
shadowRoot.getElementById("sink-list").getElementsByTagName("span");
for (var i=0; i<sinks.length; i++) {
if(sinks[i].textContent.trim() == {{ sink_name }}) {
sinks[i].click();
break;
}}
""",
sink_name=sink_name)
def CloseDialog(self, tab):
"""Closes media router dialog."""
try:
tab.ExecuteJavaScript(
'window.document.getElementById("media-router-container").' +
'shadowRoot.getElementById("container-header").shadowRoot.' +
'getElementById("close-button").click();')
except (exceptions.DevtoolsTargetCrashException,
exceptions.EvaluateException,
exceptions.TimeoutException):
# Ignore the crash exception, this exception is caused by the js
# code which closes the dialog, it is expected.
# Ignore the evaluate exception, this exception maybe caused by the dialog
# is closed/closing when the JS is executing.
# Ignore the timeout exception, this exception can be caused by finding
# the close-button on a dialog that is already closed.
pass
def CloseExistingRoute(self, action_runner, sink_name):
"""Closes the existing route if it exists, otherwise does nothing."""
action_runner.TapElement(selector='#start_session_button')
action_runner.Wait(5)
for tab in action_runner.tab.browser.tabs:
if tab.url == 'chrome://media-router/':
if self.CheckIfExistingRoute(tab, sink_name):
self.ChooseSink(tab, sink_name)
tab.ExecuteJavaScript(
"window.document.getElementById('media-router-container')."
"shadowRoot.getElementById('route-details').shadowRoot."
"getElementById('close-route-button').click();")
self.CloseDialog(tab)
# Wait for 5s to make sure the route is closed.
action_runner.Wait(5)
def CheckIfExistingRoute(self, tab, sink_name):
""""Checks if there is existing route for the specific sink."""
tab.ExecuteJavaScript("""
var sinks = window.document.getElementById('media-router-container').
allSinks;
var sink_id = null;
for (var i=0; i<sinks.length; i++) {
if (sinks[i].name == {{ sink_name }}) {
console.info('sink id: ' + sinks[i].id);
sink_id = sinks[i].id;
break;
}
}
var routes = window.document.getElementById('media-router-container').
routeList;
for (var i=0; i<routes.length; i++) {
if (!!sink_id && routes[i].sinkId == sink_id) {
window.__telemetry_route_id = routes[i].id;
break;
}
}""",
sink_name=sink_name)
route = tab.EvaluateJavaScript('!!window.__telemetry_route_id')
logging.info('Is there existing route? ' + str(route))
return route
def ExecuteAsyncJavaScript(self, action_runner, script, verify_func,
error_message, timeout=5, retry=1):
"""Executes async javascript function and waits until it finishes."""
exception = None
for _ in xrange(retry):
try:
action_runner.ExecuteJavaScript(script)
self._WaitForResult(
action_runner, verify_func, error_message, timeout=timeout)
exception = None
break
except RuntimeError as e:
exception = e
if exception:
raise Exception(exception)
def WaitUntilDialogLoaded(self, action_runner, tab):
"""Waits until dialog is fully loaded."""
self._WaitForResult(
action_runner,
lambda: tab.EvaluateJavaScript(
'!!window.document.getElementById('
'"media-router-container") &&'
'window.document.getElementById('
'"media-router-container").sinksToShow_ &&'
'window.document.getElementById('
'"media-router-container").sinksToShow_.length'),
'The dialog is not fully loaded within 15s.',
timeout=15)
def _WaitForResult(self, action_runner, verify_func, error_message,
timeout=5):
"""Waits until the function finishes or timeout."""
start_time = time.time()
while (not verify_func() and
time.time() - start_time < timeout):
action_runner.Wait(1)
if not verify_func():
raise RuntimeError(error_message)
def _GetOSEnviron(self, environ_variable):
"""Gets an OS environment variable on the machine."""
if (environ_variable not in os.environ or
not os.environ.get(environ_variable)):
raise RuntimeError(
'Your test machine is not set up correctly, '
'%s enviroment variable is missing.', environ_variable)
return os.environ[environ_variable]
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
from core import path_util
from core import perf_benchmark
from telemetry import benchmark
from telemetry import story
from contrib.media_router_benchmarks import media_router_measurements
from contrib.media_router_benchmarks import media_router_pages
@benchmark.Info(emails=['mfoltz@chromium.org', 'cliffordcheng@chromium.org'],
component='Internals>Cast')
class MediaRouterCPUMemoryCast(perf_benchmark.PerfBenchmark):
"""Obtains media performance for key user scenarios on desktop."""
SUPPORTED_PLATFORMS = [story.expectations.ALL_DESKTOP]
options = {'pageset_repeat': 1}
page_set = media_router_pages.MediaRouterCPUMemoryPageSet
def SetExtraBrowserOptions(self, options):
options.clear_sytem_cache_for_browser_and_profile_on_start = True
# This flag is required to enable the communication between the page and
# the test extension.
options.disable_background_networking = False
options.AppendExtraBrowserArgs([
'--load-extension=' + ','.join(
[os.path.join(path_util.GetChromiumSrcDir(), 'out',
'Release', 'mr_extension', 'release'),
os.path.join(path_util.GetChromiumSrcDir(), 'out',
'Release', 'media_router', 'media_router_telemetry_extension')]),
'--disable-features=ViewsCastDialog',
'--whitelisted-extension-id=enhhojjnijigcajfphajepfemndkmdlo',
'--media-router=1',
'--enable-stats-collection-bindings'
])
def CreatePageTest(self, options):
return media_router_measurements.MediaRouterCPUMemoryTest()
@classmethod
def Name(cls):
return 'media_router.cpu_memory'
@benchmark.Info(emails=['mfoltz@chromium.org', 'cliffordcheng@chromium.org'],
component='Internals>Cast')
class NoMediaRouterCPUMemory(perf_benchmark.PerfBenchmark):
"""Benchmark for CPU and memory usage without Media Router."""
SUPPORTED_PLATFORMS = [story.expectations.ALL_DESKTOP]
options = {'pageset_repeat': 1}
page_set = media_router_pages.CPUMemoryPageSet
def SetExtraBrowserOptions(self, options):
options.clear_sytem_cache_for_browser_and_profile_on_start = True
# This flag is required to enable the communication between the page and
# the test extension.
options.disable_background_networking = False
options.AppendExtraBrowserArgs([
'--load-extension=' +
os.path.join(path_util.GetChromiumSrcDir(), 'out',
'Release', 'media_router', 'media_router_telemetry_extension'),
'--disable-features=ViewsCastDialog',
'--media-router=0',
'--enable-stats-collection-bindings'
])
def CreatePageTest(self, options):
return media_router_measurements.MediaRouterCPUMemoryTest()
@classmethod
def Name(cls):
return 'media_router.cpu_memory.no_media_router'
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import json
from telemetry.core import exceptions
from telemetry.value import scalar
from metrics import Metric
METRICS = {'privateMemory': {'units': 'MB', 'display_name': 'private_memory'},
'cpu': {'units': '%', 'display_name': 'cpu_utilization'}}
class MediaRouterCPUMemoryMetric(Metric):
"A metric for media router CPU/Memory usage."
def Start(self, page, tab):
raise NotImplementedError()
def Stop(self, page, tab):
raise NotImplementedError()
def AddResults(self, tab, results):
results_json = None
try:
results_json = tab.EvaluateJavaScript(
'JSON.stringify(window.perfResults)')
except exceptions.EvaluateException:
pass
# This log gives the detailed information about CPU/memory usage.
logging.info('results_json' + ': ' + str(results_json))
if not results_json:
return
perf_results = json.loads(results_json)
for (metric, metric_results) in perf_results.iteritems():
for (process, process_results) in metric_results.iteritems():
if not process_results:
continue
avg_result = round(sum(process_results)/len(process_results), 4)
if metric == 'privateMemory':
avg_result = round(avg_result/(1024 * 1024), 2)
logging.info('metric: %s, process: %s, average value: %s' %
(metric, process, str(avg_result)))
results.AddValue(scalar.ScalarValue(
results.current_page,
'%s_%s' % (METRICS.get(metric).get('display_name'), process),
METRICS.get(metric).get('units'),
avg_result))
# Calculate MR extension wakeup time
if 'mr_extension' in perf_results['cpu']:
wakeup_percentage = round(
(len(perf_results['cpu']['mr_extension']) * 100 /
len(perf_results['cpu']['browser'])), 2)
results.AddValue(scalar.ScalarValue(
results.current_page, 'mr_extension_wakeup_percentage',
'%', wakeup_percentage))
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from contrib.media_router_benchmarks import media_router_cpu_memory_metric
from telemetry.page import legacy_page_test
class MediaRouterCPUMemoryTest(legacy_page_test.LegacyPageTest):
"""Performs a measurement of Media Route CPU/memory usage."""
def __init__(self):
super(MediaRouterCPUMemoryTest, self).__init__()
self._metric = media_router_cpu_memory_metric.MediaRouterCPUMemoryMetric()
def ValidateAndMeasurePage(self, page, tab, results):
self._metric.AddResults(tab, results)
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from telemetry import story
from telemetry.page import shared_page_state
from telemetry.util import js_template
from contrib.media_router_benchmarks.media_router_base_page import MediaRouterBasePage
SESSION_TIME = 300 # 5 minutes
class SharedState(shared_page_state.SharedPageState):
"""Shared state that restarts the browser for every single story."""
def __init__(self, test, finder_options, story_set):
super(SharedState, self).__init__(
test, finder_options, story_set)
def DidRunStory(self, results):
super(SharedState, self).DidRunStory(results)
self._StopBrowser()
class CastIdlePage(MediaRouterBasePage):
"""Cast page to open a cast-enabled page and do nothing."""
def __init__(self, page_set):
super(CastIdlePage, self).__init__(
page_set=page_set,
url='file://test_site/basic_test.html',
shared_page_state_class=SharedState,
name='basic_test.html')
def RunPageInteractions(self, action_runner):
# Wait for 5s after Chrome is opened in order to get consistent results.
action_runner.Wait(5)
with action_runner.CreateInteraction('Idle'):
action_runner.ExecuteJavaScript('collectPerfData();')
action_runner.Wait(SESSION_TIME)
class CastFlingingPage(MediaRouterBasePage):
"""Cast page to fling a video to Chromecast device."""
def __init__(self, page_set):
super(CastFlingingPage, self).__init__(
page_set=page_set,
url='file://test_site/basic_test.html#flinging',
shared_page_state_class=SharedState,
name='basic_test.html#flinging')
def RunPageInteractions(self, action_runner):
sink_name = self._GetOSEnviron('RECEIVER_NAME')
# Wait for 5s after Chrome is opened in order to get consistent results.
action_runner.Wait(5)
with action_runner.CreateInteraction('flinging'):
self._WaitForResult(
action_runner,
lambda: action_runner.EvaluateJavaScript('initialized'),
'Failed to initialize',
timeout=30)
self.CloseExistingRoute(action_runner, sink_name)
# Start session
action_runner.TapElement(selector='#start_session_button')
self._WaitForResult(
action_runner,
lambda: len(action_runner.tab.browser.tabs) >= 2,
'MR dialog never showed up.')
for tab in action_runner.tab.browser.tabs:
# Choose sink
if tab.url == 'chrome://media-router/':
self.WaitUntilDialogLoaded(action_runner, tab)
self.ChooseSink(tab, sink_name)
self._WaitForResult(
action_runner,
lambda: action_runner.EvaluateJavaScript('currentSession'),
'Failed to start session',
timeout=10)
# Load Media
self.ExecuteAsyncJavaScript(
action_runner,
js_template.Render(
'loadMedia({{ url }});', url=self._GetOSEnviron('VIDEO_URL')),
lambda: action_runner.EvaluateJavaScript('currentMedia'),
'Failed to load media',
timeout=120)
action_runner.Wait(5)
action_runner.ExecuteJavaScript('collectPerfData();')
action_runner.Wait(SESSION_TIME)
# Stop session
self.ExecuteAsyncJavaScript(
action_runner,
'stopSession();',
lambda: not action_runner.EvaluateJavaScript('currentSession'),
'Failed to stop session',
timeout=60, retry=3)
class CastMirroringPage(MediaRouterBasePage):
"""Cast page to mirror a tab to Chromecast device."""
def __init__(self, page_set):
super(CastMirroringPage, self).__init__(
page_set=page_set,
url='file://test_site/mirroring.html',
shared_page_state_class=SharedState,
name='mirroring.html')
def RunPageInteractions(self, action_runner):
sink_name = self._GetOSEnviron('RECEIVER_NAME')
# Wait for 5s after Chrome is opened in order to get consistent results.
action_runner.Wait(5)
with action_runner.CreateInteraction('mirroring'):
self.CloseExistingRoute(action_runner, sink_name)
# Start session
action_runner.TapElement(selector='#start_session_button')
self._WaitForResult(
action_runner,
lambda: len(action_runner.tab.browser.tabs) >= 2,
'MR dialog never showed up.')
for tab in action_runner.tab.browser.tabs:
# Choose sink
if tab.url == 'chrome://media-router/':
self.WaitUntilDialogLoaded(action_runner, tab)
self.ChooseSink(tab, sink_name)
# Wait for 5s to make sure the route is created.
action_runner.Wait(5)
action_runner.TapElement(selector='#start_session_button')
self._WaitForResult(
action_runner,
lambda: len(action_runner.tab.browser.tabs) >= 2,
'MR dialog never showed up.')
for tab in action_runner.tab.browser.tabs:
if tab.url == 'chrome://media-router/':
self.WaitUntilDialogLoaded(action_runner, tab)
if not self.CheckIfExistingRoute(tab, sink_name):
raise RuntimeError('Failed to start mirroring session.')
action_runner.ExecuteJavaScript('collectPerfData();')
action_runner.Wait(SESSION_TIME)
self.CloseExistingRoute(action_runner, sink_name)
class MediaRouterCPUMemoryPageSet(story.StorySet):
"""Pageset for media router CPU/memory usage tests."""
def __init__(self):
super(MediaRouterCPUMemoryPageSet, self).__init__(
cloud_storage_bucket=story.PARTNER_BUCKET)
self.AddStory(CastIdlePage(self))
self.AddStory(CastFlingingPage(self))
self.AddStory(CastMirroringPage(self))
class CPUMemoryPageSet(story.StorySet):
"""Pageset to get baseline CPU and memory usage."""
def __init__(self):
super(CPUMemoryPageSet, self).__init__(
cloud_storage_bucket=story.PARTNER_BUCKET)
self.AddStory(CastIdlePage(self))
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>MR Integration Basic Test</title>
<script type="text/javascript" src="common.js"></script>
<script type="text/javascript" src="cpu_memory_script.js"></script>
<script type="text/javascript"
src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js">
</script>
</head>
<body>
<button id="start_session_button" onclick="startFlingingSession()">
Start session
</button>
</body>
</html>
/**
* Copyright 2019 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* @fileoverview Common APIs for media router performance tests.
*
*/
var initialized = false;
var currentSession = null;
var currentMedia = null;
window['__onGCastApiAvailable'] = function(loaded, errorInfo) {
if (loaded) {
initializeCastApi();
} else {
console.log(errorInfo);
}
}
/**
* Initialize Cast APIs.
*/
function initializeCastApi() {
// Load Cast APIs
console.info('Initializing API');
var sessionRequest = new chrome.cast.SessionRequest(
chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
var apiConfig = new chrome.cast.ApiConfig(
sessionRequest,
null, // session listener
function(availability) { // receiver listener
console.info('Receiver listener: ' + JSON.stringify(availability));
initialized = true;
});
chrome.cast.initialize(
apiConfig,
function() { // Successful callback
console.info('Initialize successfully');
},
function(error) { // Error callback
console.error('Initialize failed, error: ' + JSON.stringify(error));
});
}
/**
* Start a new session for flinging scenario.
*/
function startFlingingSession() {
console.info('Starting Session');
chrome.cast.requestSession(
function(session) { // Request session successful callback
console.info('Request session successfully');
currentSession = session;
},
function(error) { // Request session Error callback
console.error('Request session failed, error: ' + JSON.stringify(error));
});
}
/**
* Loads the specific video on Chromecast.
*
* @param {string} mediaUrl the url which points to a mp4 video.
*/
function loadMedia(mediaUrl) {
if (!currentSession) {
console.warn('Cannot load media without a live session');
}
console.info('loading ' + mediaUrl);
var mediaInfo = new chrome.cast.media.MediaInfo(mediaUrl, 'video/mp4');
var request = new chrome.cast.media.LoadRequest(mediaInfo);
request.autoplay = true;
request.currentTime = 0;
currentSession.loadMedia(request,
function(media) {
console.info('Load media successfully');
currentMedia = media;
},
function(error) { // Error callback
console.error('Load media failed, error: ' + JSON.stringify(error));
});
}
/**
* Stops current session.
*/
function stopSession() {
if (currentSession) {
currentSession.stop(
function() {
console.info('Stop session successfully');
currentSession = null;
},
function(error) { // Error callback
console.error('Stop session failed, error: ' + JSON.stringify(error));
});
}
}
/**
* Copyright 2019 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* @fileoverview Script to interact with test extension to get CPU/memory usage.
*
*/
// MR test extension ID.
var extensionId = 'ocihafebdemjoofhbdnamghkfobfgbal';
/**
* The dictionary to store the performance results with the following format:
* key: the metric, e.g. CPU, private memory
* value: map of the performance results for different processes.
* key: process type, e.g. tab, browser, gpu, extension.
* value: list of the performance results per second. Task Manager notifies
* the event listener nearly every second for the latest status of
* each process.
*/
window.perfResults = {};
/**
* Connects to the test extension and starts to collect CPU/memory usage data.
*/
function collectPerfData() {
processCpuPort_ = openPort_('collectData');
if (processCpuPort_) {
processCpuPort_.onMessage.addListener(function(message) {
for (metric in message) {
if (!window.perfResults[metric]) {
window.perfResults[metric] = {};
}
for (process_type in message[metric]) {
if (!window.perfResults[metric][process_type]) {
window.perfResults[metric][process_type] = []
}
window.perfResults[metric][process_type].push(
message[metric][process_type]);
}
}
});
} else {
console.log('Unable to connect to port');
}
}
function openPort_(name) {
var rt = window.chrome.runtime;
if (rt && rt.connect) {
console.info('Opening port named ' + name);
return rt.connect(extensionId, {'name': name});
}
return null;
}
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>MR Perf Test</title>
<script type="text/javascript" src="cpu_memory_script.js"></script>
<script>
function startSession() {
var startSessionRequest = new PresentationRequest("");
startSessionRequest.start();
console.log('start session');
}
</script>
</head>
<body>
<button id="start_session_button" onclick="startSession()">
Start session
</button>
<video id="video_player" controls autoplay loop src="bear-vp9-opus.webm">
</video>
</body>
</html>
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