Commit 95483314 authored by wez@chromium.org's avatar wez@chromium.org

Add a Record button to the web-app if the host supports video recording.

Video recording allows sequences of frames to be recorded and delivered
to the client for performance-evaluation purposes. The client can save
recorded sequences out to files on disk. Since live playback is not a
requirement, performance of the implementation is not a priority.

See crrev.com/372943002 for the host side of this.

Review URL: https://codereview.chromium.org/386853002

Cr-Commit-Position: refs/heads/master@{#290721}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@290721 0039d316-1c4b-4281-b951-d872f2087c98
parent c90b329c
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
'webapp/media_source_renderer.js', 'webapp/media_source_renderer.js',
'webapp/session_connector.js', 'webapp/session_connector.js',
'webapp/smart_reconnector.js', 'webapp/smart_reconnector.js',
'webapp/video_frame_recorder.js',
], ],
# Remoting core JavaScript files. # Remoting core JavaScript files.
'remoting_webapp_js_core_files': [ 'remoting_webapp_js_core_files': [
......
...@@ -716,6 +716,12 @@ For information about privacy, please see the Google Privacy Policy (http://goo. ...@@ -716,6 +716,12 @@ For information about privacy, please see the Google Privacy Policy (http://goo.
<message desc="Label for button to reconnect to the previous Me2Me host. This button appears on the 'session-finished' page." name="IDS_RECONNECT"> <message desc="Label for button to reconnect to the previous Me2Me host. This button appears on the 'session-finished' page." name="IDS_RECONNECT">
Reconnect Reconnect
</message> </message>
<message desc="Button for starting video frame recording, to capture frame sequences for debugging purposes." name="IDS_START_RECORDING">
Start Recording
</message>
<message desc="Button for stopping recording of video frames." name="IDS_STOP_RECORDING">
Stop Recording
</message>
<message desc="Button for enabling or disabling the 'resize-to-client' functionality, whereby the host desktop is resized to match the client size as closely as possible." name="IDS_RESIZE_TO_CLIENT"> <message desc="Button for enabling or disabling the 'resize-to-client' functionality, whereby the host desktop is resized to match the client size as closely as possible." name="IDS_RESIZE_TO_CLIENT">
Resize desktop to fit Resize desktop to fit
</message> </message>
......
...@@ -371,6 +371,9 @@ remoting.onConnected = function(clientSession) { ...@@ -371,6 +371,9 @@ remoting.onConnected = function(clientSession) {
* @return {boolean} Return true if the extension message was recognized. * @return {boolean} Return true if the extension message was recognized.
*/ */
remoting.onExtensionMessage = function(type, data) { remoting.onExtensionMessage = function(type, data) {
if (remoting.clientSession) {
return remoting.clientSession.handleExtensionMessage(type, data);
}
return false; return false;
}; };
......
...@@ -176,6 +176,9 @@ remoting.ClientSession = function(container, hostDisplayName, accessCode, ...@@ -176,6 +176,9 @@ remoting.ClientSession = function(container, hostDisplayName, accessCode,
/** @type {remoting.CastExtensionHandler} @private */ /** @type {remoting.CastExtensionHandler} @private */
this.castExtensionHandler_ = null; this.castExtensionHandler_ = null;
/** @type {remoting.VideoFrameRecorder} @private */
this.videoFrameRecorder_ = null;
if (this.mode_ == remoting.ClientSession.Mode.IT2ME) { if (this.mode_ == remoting.ClientSession.Mode.IT2ME) {
// Resize-to-client is not supported for IT2Me hosts. // Resize-to-client is not supported for IT2Me hosts.
this.resizeToClientButton_.hidden = true; this.resizeToClientButton_.hidden = true;
...@@ -185,6 +188,7 @@ remoting.ClientSession = function(container, hostDisplayName, accessCode, ...@@ -185,6 +188,7 @@ remoting.ClientSession = function(container, hostDisplayName, accessCode,
this.fullScreenButton_.addEventListener( this.fullScreenButton_.addEventListener(
'click', this.callToggleFullScreen_, false); 'click', this.callToggleFullScreen_, false);
this.defineEvents(Object.keys(remoting.ClientSession.Events)); this.defineEvents(Object.keys(remoting.ClientSession.Events));
}; };
...@@ -1054,6 +1058,10 @@ remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) { ...@@ -1054,6 +1058,10 @@ remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
clientArea.height, clientArea.height,
window.devicePixelRatio); window.devicePixelRatio);
} }
if (this.hasCapability_(
remoting.ClientSession.Capability.VIDEO_RECORDER)) {
this.videoFrameRecorder_ = new remoting.VideoFrameRecorder(this.plugin_);
}
}; };
/** /**
...@@ -1593,3 +1601,44 @@ remoting.ClientSession.prototype.createCastExtensionHandler_ = function() { ...@@ -1593,3 +1601,44 @@ remoting.ClientSession.prototype.createCastExtensionHandler_ = function() {
} }
}; };
/**
* Returns true if the ClientSession can record video frames to a file.
* @return {boolean}
*/
remoting.ClientSession.prototype.canRecordVideo = function() {
return !!this.videoFrameRecorder_;
}
/**
* Returns true if the ClientSession is currently recording video frames.
* @return {boolean}
*/
remoting.ClientSession.prototype.isRecordingVideo = function() {
if (!this.videoFrameRecorder_) {
return false;
}
return this.videoFrameRecorder_.isRecording();
}
/**
* Starts or stops recording of video frames.
*/
remoting.ClientSession.prototype.startStopRecording = function() {
if (this.videoFrameRecorder_) {
this.videoFrameRecorder_.startStopRecording();
}
}
/**
* Handles protocol extension messages.
* @param {string} type Type of extension message.
* @param {string} data Contents of the extension message.
* @return {boolean} True if the message was recognized, false otherwise.
*/
remoting.ClientSession.prototype.handleExtensionMessage =
function(type, data) {
if (this.videoFrameRecorder_) {
return this.videoFrameRecorder_.handleMessage(type, data);
}
return false;
}
...@@ -13,6 +13,9 @@ found in the LICENSE file. ...@@ -13,6 +13,9 @@ found in the LICENSE file.
<img src="icon_options.webp"> <img src="icon_options.webp">
</span> </span>
<ul class="window-options-menu right-align"> <ul class="window-options-menu right-align">
<li class="menu-start-stop-recording"
i18n-content="START_RECORDING"
hidden></li>
<li class="menu-send-ctrl-alt-del" <li class="menu-send-ctrl-alt-del"
i18n-content="SEND_CTRL_ALT_DEL"></li> i18n-content="SEND_CTRL_ALT_DEL"></li>
<li class="menu-send-print-screen" <li class="menu-send-print-screen"
......
...@@ -240,6 +240,20 @@ OnClickData.prototype.wasChecked; ...@@ -240,6 +240,20 @@ OnClickData.prototype.wasChecked;
OnClickData.prototype.checked; OnClickData.prototype.checked;
/** @type {Object} */
chrome.fileSystem = {
/**
* @param {Object.<string>?} options
* @param {function(Entry, Array.<FileEntry>):void} callback
*/
chooseEntry: function(options, callback) {},
/**
* @param {FileEntry} fileEntry
* @param {function(string):void} callback
*/
getDisplayPath: function(fileEntry, callback) {}
};
/** @type {Object} */ /** @type {Object} */
chrome.identity = { chrome.identity = {
/** /**
...@@ -507,4 +521,3 @@ chrome.cast.initialize = ...@@ -507,4 +521,3 @@ chrome.cast.initialize =
*/ */
chrome.cast.requestSession = chrome.cast.requestSession =
function(successCallback, errorCallback) {}; function(successCallback, errorCallback) {};
...@@ -77,6 +77,7 @@ ...@@ -77,6 +77,7 @@
"nativeMessaging" "nativeMessaging"
{% if webapp_type != 'v1' %} {% if webapp_type != 'v1' %}
, ,
{"fileSystem": ["write"]},
"fullscreen", "fullscreen",
"identity", "identity",
"contextMenus", "contextMenus",
......
...@@ -19,17 +19,20 @@ var remoting = remoting || {}; ...@@ -19,17 +19,20 @@ var remoting = remoting || {};
* @param {Element} shrinkToFit * @param {Element} shrinkToFit
* @param {Element} newConnection * @param {Element} newConnection
* @param {Element?} fullscreen * @param {Element?} fullscreen
* @param {Element?} startStopRecording
* @constructor * @constructor
*/ */
remoting.OptionsMenu = function(sendCtrlAltDel, sendPrtScrn, remoting.OptionsMenu = function(sendCtrlAltDel, sendPrtScrn,
resizeToClient, shrinkToFit, resizeToClient, shrinkToFit,
newConnection, fullscreen) { newConnection, fullscreen,
startStopRecording) {
this.sendCtrlAltDel_ = sendCtrlAltDel; this.sendCtrlAltDel_ = sendCtrlAltDel;
this.sendPrtScrn_ = sendPrtScrn; this.sendPrtScrn_ = sendPrtScrn;
this.resizeToClient_ = resizeToClient; this.resizeToClient_ = resizeToClient;
this.shrinkToFit_ = shrinkToFit; this.shrinkToFit_ = shrinkToFit;
this.newConnection_ = newConnection; this.newConnection_ = newConnection;
this.fullscreen_ = fullscreen; this.fullscreen_ = fullscreen;
this.startStopRecording_ = startStopRecording;
/** /**
* @type {remoting.ClientSession} * @type {remoting.ClientSession}
* @private * @private
...@@ -50,6 +53,10 @@ remoting.OptionsMenu = function(sendCtrlAltDel, sendPrtScrn, ...@@ -50,6 +53,10 @@ remoting.OptionsMenu = function(sendCtrlAltDel, sendPrtScrn,
this.fullscreen_.addEventListener( this.fullscreen_.addEventListener(
'click', this.onFullscreen_.bind(this), false); 'click', this.onFullscreen_.bind(this), false);
} }
if (this.startStopRecording_) {
this.startStopRecording_.addEventListener(
'click', this.onStartStopRecording_.bind(this), false);
}
}; };
/** /**
...@@ -70,6 +77,16 @@ remoting.OptionsMenu.prototype.onShow = function() { ...@@ -70,6 +77,16 @@ remoting.OptionsMenu.prototype.onShow = function() {
remoting.MenuButton.select( remoting.MenuButton.select(
this.fullscreen_, remoting.fullscreen.isActive()); this.fullscreen_, remoting.fullscreen.isActive());
} }
if (this.startStopRecording_) {
this.startStopRecording_.hidden = !this.clientSession_.canRecordVideo();
if (this.clientSession_.isRecordingVideo()) {
l10n.localizeElementFromTag(this.startStopRecording_,
/*i18n-content*/'STOP_RECORDING');
} else {
l10n.localizeElementFromTag(this.startStopRecording_,
/*i18n-content*/'START_RECORDING');
}
}
} }
}; };
...@@ -110,3 +127,9 @@ remoting.OptionsMenu.prototype.onNewConnection_ = function() { ...@@ -110,3 +127,9 @@ remoting.OptionsMenu.prototype.onNewConnection_ = function() {
remoting.OptionsMenu.prototype.onFullscreen_ = function() { remoting.OptionsMenu.prototype.onFullscreen_ = function() {
remoting.fullscreen.toggle(); remoting.fullscreen.toggle();
}; };
remoting.OptionsMenu.prototype.onStartStopRecording_ = function() {
if (this.clientSession_) {
this.clientSession_.startStopRecording();
}
}
...@@ -52,7 +52,8 @@ remoting.Toolbar = function(toolbar) { ...@@ -52,7 +52,8 @@ remoting.Toolbar = function(toolbar) {
document.getElementById('screen-resize-to-client'), document.getElementById('screen-resize-to-client'),
document.getElementById('screen-shrink-to-fit'), document.getElementById('screen-shrink-to-fit'),
document.getElementById('new-connection'), document.getElementById('new-connection'),
document.getElementById('toggle-full-screen')); document.getElementById('toggle-full-screen'),
null);
window.addEventListener('mousemove', remoting.Toolbar.onMouseMove, false); window.addEventListener('mousemove', remoting.Toolbar.onMouseMove, false);
window.addEventListener('resize', this.center.bind(this), false); window.addEventListener('resize', this.center.bind(this), false);
......
// Copyright 2014 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
* Class implement the video frame recorder extension client.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @constructor
* @param {remoting.ClientPlugin} plugin
*/
remoting.VideoFrameRecorder = function(plugin) {
this.fileWriter_ = null;
this.isRecording_ = false;
this.plugin_ = plugin;
};
/**
* Starts or stops video recording.
*/
remoting.VideoFrameRecorder.prototype.startStopRecording = function() {
var data = {};
if (this.isRecording_) {
this.isRecording_ = false;
data = { type: 'stop' }
chrome.fileSystem.chooseEntry(
{type: 'saveFile', suggestedName: 'videoRecording.pb'},
this.onFileChosen_.bind(this));
} else {
this.isRecording_ = true;
data = { type: 'start' }
}
this.plugin_.sendClientMessage('video-recorder', JSON.stringify(data));
}
/**
* Returns true if the session is currently being recorded.
* @return {boolean}
*/
remoting.VideoFrameRecorder.prototype.isRecording = function() {
return this.isRecording_;
}
/**
* Handles 'video-recorder' extension messages and returns true. Returns
* false for all other message types.
* @param {string} type Type of extension message.
* @param {string} data Content of the extension message.
* @return {boolean}
*/
remoting.VideoFrameRecorder.prototype.handleMessage =
function(type, data) {
if (type != 'video-recorder') {
return false;
}
var message = getJsonObjectFromString(data);
var messageType = getStringAttr(message, 'type');
var messageData = getStringAttr(message, 'data');
if (messageType == 'next-frame-reply') {
if (!this.fileWriter_) {
console.log("Received frame but have no writer");
return true;
}
if (!messageData) {
console.log("Finished receiving frames");
this.fileWriter_ = null;
return true;
}
console.log("Received frame");
/* jscompile gets confused if you refer to this as just atob(). */
var videoPacketString = /** @type {string?} */ window.atob(messageData);
console.log("Converted from Base64 - length:" + videoPacketString.length);
var byteArrays = [];
for (var offset = 0; offset < videoPacketString.length; offset += 512) {
var slice = videoPacketString.slice(offset, offset + 512);
var byteNumbers = new Array(slice.length);
for (var i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
console.log("Writing frame");
videoPacketString = null;
/**
* Our current compiler toolchain only understands the old (deprecated)
* Blob constructor, which does not accept any parameters.
* TODO(wez): Remove this when compiler is updated (see crbug.com/405298).
* @suppress {checkTypes}
* @param {Array} parts
* @return {Blob}
*/
var makeBlob = function(parts) {
return new Blob(parts);
}
var videoPacketBlob = makeBlob(byteArrays);
byteArrays = null;
this.fileWriter_.write(videoPacketBlob);
return true;
}
console.log("Unrecognized message: " + messageType);
return true;
}
/** @param {FileEntry} fileEntry */
remoting.VideoFrameRecorder.prototype.onFileChosen_ = function(fileEntry) {
if (!fileEntry) {
console.log("Cancelled save of video frames.");
} else {
/** @type {function(string):void} */
chrome.fileSystem.getDisplayPath(fileEntry, function(path) {
console.log("Saving video frames to:" + path);
});
fileEntry.createWriter(this.onFileWriter_.bind(this));
}
}
/** @param {FileWriter} fileWriter */
remoting.VideoFrameRecorder.prototype.onFileWriter_ = function(fileWriter) {
console.log("Obtained FileWriter for video frame write");
fileWriter.onwriteend = this.onWriteComplete_.bind(this);
this.fileWriter_ = fileWriter;
this.fetchNextFrame_();
}
remoting.VideoFrameRecorder.prototype.onWriteComplete_ = function(e) {
console.log("Video frame write complete");
this.fetchNextFrame_();
}
remoting.VideoFrameRecorder.prototype.fetchNextFrame_ = function() {
console.log("Request next video frame");
var data = { type: 'next-frame' }
this.plugin_.sendClientMessage('video-recorder', JSON.stringify(data));
}
...@@ -47,7 +47,8 @@ remoting.WindowFrame = function(titleBar) { ...@@ -47,7 +47,8 @@ remoting.WindowFrame = function(titleBar) {
titleBar.querySelector('.menu-resize-to-client'), titleBar.querySelector('.menu-resize-to-client'),
titleBar.querySelector('.menu-shrink-to-fit'), titleBar.querySelector('.menu-shrink-to-fit'),
titleBar.querySelector('.menu-new-connection'), titleBar.querySelector('.menu-new-connection'),
null); null,
titleBar.querySelector('.menu-start-stop-recording'));
/** /**
* @type {HTMLElement} * @type {HTMLElement}
......
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