src/dynamics/video_recorder/recorder/recorder.js
Scoped.define("module:VideoRecorder.Dynamics.Recorder", [
"dynamics:Dynamic",
"module:Assets",
"module:StylesMixin",
"browser:Info",
"browser:Dom",
"browser:Events",
"browser:Upload.MultiUploader",
"browser:Upload.FileUploader",
"media:Recorder.VideoRecorderWrapper",
"media:Recorder.Support",
"media:WebRTC.Support",
"base:Types",
"base:Objs",
"base:Strings",
"base:Time",
"base:Timers",
"base:States.Host",
"base:Classes.ClassRegistry",
"base:Collections.Collection",
"base:Promise",
"module:VideoRecorder.Dynamics.RecorderStates.Initial",
"module:VideoRecorder.Dynamics.RecorderStates"
], [
"module:VideoRecorder.Dynamics.Imagegallery",
"module:VideoRecorder.Dynamics.Loader",
"module:VideoRecorder.Dynamics.Controlbar",
"module:VideoRecorder.Dynamics.Message",
"module:VideoRecorder.Dynamics.Topmessage",
"module:VideoRecorder.Dynamics.Chooser",
"module:VideoRecorder.Dynamics.Faceoutline",
"module:Common.Dynamics.Helperframe",
"dynamics:Partials.ShowPartial",
"dynamics:Partials.IfPartial",
"dynamics:Partials.EventPartial",
"dynamics:Partials.OnPartial",
"dynamics:Partials.DataPartial",
"dynamics:Partials.AttrsPartial",
"dynamics:Partials.StylesPartial",
"dynamics:Partials.TemplatePartial",
"dynamics:Partials.HotkeyPartial"
], function(Class, Assets, StylesMixin, Info, Dom, DomEvents, MultiUploader, FileUploader, VideoRecorderWrapper, RecorderSupport, WebRTCSupport, Types, Objs, Strings, Time, Timers, Host, ClassRegistry, Collection, Promise, InitialState, RecorderStates, scoped) {
return Class.extend({
scoped: scoped
}, [StylesMixin, function(inherited) {
return {
template: "<%= template(dirname + '/recorder.html') %>",
attrs: {
/* CSS */
"css": "ba-videorecorder",
"csscommon": "ba-commoncss",
"cssrecorder": "ba-recorder",
"iecss": "ba-videorecorder",
"cssimagegallery": "",
"cssloader": "",
"csscontrolbar": "",
"cssmessage": "",
"csstopmessage": "",
"csschooser": "",
"csshelperframe": "",
"gallerysnapshots": 3,
"popup-width": "",
"popup-height": "",
/* Themes */
"theme": "",
"csstheme": "",
"themecolor": "",
/* Dynamics */
"dynimagegallery": "videorecorder-imagegallery",
"dynloader": "videorecorder-loader",
"dyncontrolbar": "videorecorder-controlbar",
"dynmessage": "videorecorder-message",
"dyntopmessage": "videorecorder-topmessage",
"dynchooser": "videorecorder-chooser",
"dynvideoplayer": "videoplayer",
"dynhelperframe": "helperframe",
/* Templates */
"tmplimagegallery": "",
"tmplloader": "",
"tmplcontrolbar": "",
"tmplmessage": "",
"tmpltopmessage": "",
"tmplchooser": "",
"tmplhelperframe": "",
/* Attributes */
"autorecord": false,
"autoplay": false,
"allowrecord": true,
"allowupload": true,
"allowcustomupload": true,
"manual-upload": false,
"camerafacefront": false,
"fittodimensions": false,
"resizemode": null, // enum option to scale screen recorder, has 2 options: 'crop-and-scale', 'none'
"createthumbnails": false,
"primaryrecord": true,
"allowscreen": false,
"initialmessages": [], // should include object at least with message key, and optional type with enum: "error", "warninig" (default) or "success"
"screenrecordmandatory": false,
"nofullscreen": false,
"recordingwidth": undefined,
"recordingheight": undefined,
"minuploadingwidth": undefined,
"maxuploadingwidth": undefined,
"minuploadingheight": undefined,
"maxuploadingheight": undefined,
"countdown": 3,
"snapshotmax": 15,
"framerate-warning": null,
"framerate": null,
"audiobitrate": null,
"videobitrate": null,
"snapshottype": "jpg",
"picksnapshots": true,
"playbacksource": "",
"screen": {},
"playbackposter": "",
"recordermode": true,
"skipinitial": false,
"skipinitialonrerecord": false,
"timelimit": null,
"timeminlimit": null,
"webrtcstreaming": false,
"webrtconmobile": false,
"webrtcstreamingifnecessary": true,
"microphone-volume": 1.0,
"flip-camera": false,
"flipscreen": false, // Will affect as true, if flip-camera also set as true
"early-rerecord": false,
"custom-covershots": false,
"selectfirstcovershotonskip": false,
"picksnapshotmandatory": false,
"media-orientation": null, // possible options "landscape", "portrait"
"manualsubmit": false,
"allowedextensions": null,
"filesizelimit": null,
"faceoutline": false,
"display-timer": true,
"pausable": false,
"sharevideo": [],
"videofitstrategy": "pad",
"posterfitstrategy": "crop",
"placeholderstyle": "",
"hasplaceholderstyle": false,
/** outsource-selectors should start with cam-* or/and mic-* will add in the feature other selectors.
* If set skipinitial devices info will be accessible w/o pressing record button
* all settings should be seperated by ';' and has to be ID selector (only characters: "-", "_", numbers and letters allowed)
* OPTIONS:
* 'type': Could be manage type of element, by default will be 'select' element
* possible types: 'radio', 'select'(default) all should be as a string
* 'disabled': Could be set disable by default, and of there're no option more than 1
* device selection. Default is true
* 'className': will be added as a class attribute to single option and in label for radio
* 'showCapabilities' for camera only and if available camera resolutions info provided
* by browser. Default is true
* examples:
* "cam-my-own-id-selector;mic-my-own-id-selector"
* "cam-my-own-id-selector[type='select',disabled=false,showCapabilities=true];mic-my-own-id-selector[type='radio',className='class1 class2 etc']"
*/
"outsource-selectors": null,
/* Configuration */
"simulate": false,
"onlyaudio": false,
"noaudio": false,
"enforce-duration": null,
"localplayback": false,
"uploadoptions": {},
"playerattrs": {},
"shortMessage": true,
"cpu-friendly": false,
/* Options */
"rerecordable": true,
"allowcancel": false,
"recordings": null,
"ready": true,
"orientation": false,
"popup": false,
"audio-test-mandatory": false,
"snapshotfromuploader": false,
"snapshotfrommobilecapture": false,
"allowmultistreams": false,
"showaddstreambutton": false,
"multistreamreversable": true,
"multistreamdraggable": true,
"multistreamresizeable": false,
"addstreamproportional": true,
"addstreampositionx": 5,
"addstreampositiony": 5,
"addstreampositionwidth": 120,
"addstreampositionheight": null,
"addstreamminwidth": 120,
"addstreamminheight": null,
"addstreamdeviceid": null,
"showsettingsmenu": true, // As a property show/hide settings from users
"showplayersettingsmenu": true, // As a property show/hide after recorder player settings from users
"allowtexttrackupload": false,
"framevisible": false,
"uploadlocales": [{
lang: 'en',
label: 'English'
}],
"tracktags": [],
"outsourceSelectors": [],
"hassubtitles": false,
"videometadata": {},
"optionsinitialstate": {},
"playerfallbackwidth": 320,
"playerfallbackheight": 240,
"pickcovershotframe": false,
"allowtrim": false,
"trimoverlay": true
},
computed: {
"nativeRecordingWidth:recordingwidth,record_media": function() {
return this.get("recordingwidth") || ((this.get("record_media") !== "screen" && (this.get("record_media") !== "multistream")) ? 640 : (window.innerWidth || document.body.clientWidth));
},
"nativeRecordingHeight:recordingheight,record_media": function() {
return this.get("recordingheight") || ((this.get("record_media") !== "screen" && (this.get("record_media") !== "multistream")) ? 480 : (window.innerHeight || document.body.clientHeight));
},
"containerSizingStyles:aspectratio,nativeRecordingWidth,nativeRecordingHeight,activated, height, width": function(aspectRatio, fallbackWidth, fallbackHeight, active, height, width) {
var result = {
aspectRatio: aspectRatio || fallbackWidth + "/" + fallbackHeight
};
if (height) result.height = typeof height === "string" && height[height.length - 1] === "%" ? height : height + "px";
if (width) result.width = typeof width === "string" && width[width.length - 1] === "%" ? width : width + "px";
if (active && (Info.isInternetExplorer() || (Info.isSafari() && Info.safariVersion() < 15))) {
new ResizeObserver(function(entries) {
this.set("height", Math.floor(entries[0].target.offsetWidth / (aspectRatio || (fallbackWidth / fallbackHeight))));
}.bind(this)).observe(this.activeElement().parentElement);
result.height = Math.floor(this.activeElement().parentElement.offsetWidth / (aspectRatio || (fallbackWidth / fallbackHeight))) + "px";
}
if (this.activeElement()) this._applyStyles(this.activeElement(), result, this.__lastContainerSizingStyles);
this.__lastContainerSizingStyles = result;
return result;
},
"canswitchcamera:recordviafilecapture": function() {
return !this.get("recordviafilecapture") && Info.isMobile();
}
},
scopes: {
player: ">[id='player']"
},
types: {
"allowscreen": "boolean",
"rerecordable": "boolean",
"ready": "boolean",
"fittodimensions": "boolean",
"autorecord": "boolean",
"autoplay": "boolean",
"allowrecord": "boolean",
"allowupload": "boolean",
"allowcustomupload": "boolean",
"primaryrecord": "boolean",
"recordermode": "boolean",
"nofullscreen": "boolean",
"skipinitialonrerecord": "boolean",
"picksnapshots": "boolean",
"localplayback": "boolean",
"camerafacefront": "boolean",
"noaudio": "boolean",
"skipinitial": "boolean",
"popup": "boolean",
"popup-width": "int",
"popup-height": "int",
"enforce-duration": "bool",
"webrtcstreaming": "boolean",
"themecolor": "string",
"webrtconmobile": "boolean",
"manual-upload": "boolean",
"webrtcstreamingifnecessary": "boolean",
"microphone-volume": "float",
"audiobitrate": "int",
"videobitrate": "int",
"minuploadingwidth": "int",
"maxuploadingwidth": "int",
"minuploadingheight": "int",
"maxuploadingheight": "int",
"framerate-warning": "int",
"snapshotfromuploader": "boolean",
"snapshotfrommobilecapture": "boolean",
"flip-camera": "boolean",
"flipscreen": "boolean",
"faceoutline": "boolean",
"early-rerecord": "boolean",
"custom-covershots": "boolean",
"picksnapshotmandatory": "boolean",
"selectfirstcovershotonskip": "boolean",
"manualsubmit": "boolean",
"simulate": "boolean",
"allowedextensions": "array",
"onlyaudio": "boolean",
"cpu-friendly": "boolean",
"allowcancel": "boolean",
"display-timer": "boolean",
"audio-test-mandatory": "boolean",
"allowtexttrackupload": "boolean",
"uploadlocales": "array",
"allowmultistreams": "boolean",
"pausable": "boolean",
"sharevideo": "array",
"multistreamreversable": "boolean",
"multistreamdraggable": "boolean",
"multistreamresizeable": "boolean",
"addstreamproportional": "boolean",
"addstreampositionx": "int",
"addstreampositiony": "int",
"addstreampositionwidth": "int",
"addstreampositionheight": "int",
"addstreamminwidth": "int",
"addstreamminheight": "int",
"showsettingsmenu": "boolean",
"showplayersettingsmenu": "boolean",
"initialmessages": "array",
"screenrecordmandatory": "boolean",
"media-orientation": "string",
"mandatoryresolutions": "array",
"pickcovershotframe": "boolean",
"allowtrim": "boolean",
"trimoverlay": "boolean",
"outsourceSelectors": []
},
extendables: ["states"],
remove_on_destroy: true,
events: {
"change:camerahealthy": function(value) {
this.trigger("camerahealth", value);
},
"change:microphonehealthy": function(value) {
this.trigger("microphonehealth", value);
},
"change:webrtconmobile": function() {
this.set("recordviafilecapture", Info.isMobile() && (!this.get("webrtconmobile") || !VideoRecorderWrapper.anySupport(this._videoRecorderWrapperOptions())));
},
"change:recordviafilecapture": function() {
if (this.get("recordviafilecapture")) {
this.set("skipinitial", false);
this.set("skipinitialonrerecord", false);
this.set("autorecord", false);
this._screenRecorderVerifier(false);
}
},
"change:placeholderstyle": function(value) {
this.set("hasplaceholderstyle", value.length > 10);
}
},
create: function() {
this._validateParameters();
// Init Audio Context
WebRTCSupport.globals();
this.set("optionsinitialstate", {
autorecord: this.get("autorecord"),
skipinitial: this.get("skipinitial")
});
if (this.get("theme")) this.set("theme", this.get("theme").toLowerCase());
if (this.get("theme") in Assets.recorderthemes) {
Objs.iter(Assets.recorderthemes[this.get("theme")], function(value, key) {
if (!this.isArgumentAttr(key))
this.set(key, value);
}, this);
}
if (!this.get("themecolor"))
this.set("themecolor", "default");
if (this.get("pausable"))
this.set("resumevisible", false);
this.set("ie8", Info.isInternetExplorer() && Info.internetExplorerVersion() < 9);
this.set("hideoverlay", false);
this.set("firefox", Info.isFirefox());
this.set("canswitchcamera", false);
this.set("recordviafilecapture", Info.isMobile() && (!this.get("webrtconmobile") || !VideoRecorderWrapper.anySupport(this._videoRecorderWrapperOptions())));
if (this.get("outsource-selectors")) {
var selectors = Objs.map(this.get("outsource-selectors").split(/;/), function(item) {
var obj = {
options: {
type: 'select',
disabled: true,
showCapabilities: true
}
};
var options = '';
var splitted = item.split("[");
var selector = splitted[0];
var camSelectorPatters = /^cam\-[A-Za-z\d\-\_]*/i;
var micSelectorPatters = /^mic\-[A-Za-z\d\-\_]*/i;
if (camSelectorPatters.test(selector)) {
obj.isCamera = true;
obj.selector = selector;
}
if (micSelectorPatters.test(selector)) {
obj.isCamera = false;
obj.selector = selector;
}
if (splitted[1]) {
options = splitted[1].replace(/\]$/, "")
.replace(/\=/g, ':').replace(/\'/g, '"')
.replace(/(\w+:)|(\w+ :)/g, function(matchedStr) {
return '"' + matchedStr.substring(0, matchedStr.length - 1) + '":';
});
}
if (options.length > 5) {
try {
var parsedOptions = JSON.parse("{" + options + "}");
obj.options = Objs.tree_extend(obj.options, parsedOptions);
} catch (e) {
console.warn("Wrong settins for 'outsource-selectors' was provided");
}
}
if (!Types.is_undefined(obj.isCamera)) {
return obj;
}
}, this);
this.set("outsourceSelectors", selectors);
}
if (this.get("recordviafilecapture")) {
this.set("skipinitial", false);
this.set("skipinitialonrerecord", false);
this.set("autorecord", false);
this._screenRecorderVerifier(false);
}
this.__attachRequested = false;
this.__activated = false;
this._bound = false;
this.__recording = false;
this.__error = null;
this.host = new Host({
stateRegistry: new ClassRegistry(this.cls.recorderStates())
});
this.host.dynamic = this;
this.host.initialize(this._initialState);
this._timer = new Timers.Timer({
context: this,
fire: this._timerFire,
delay: 250,
start: true
});
this.activeElement().style.setProperty("display", "inline-block");
this._applyStyles(this.activeElement(), this.get("containerSizingStyles"));
this.__cameraResponsive = true;
this.__cameraSignal = true;
this._initSettings();
if (this.get("onlyaudio")) {
this.set("allowupload", false);
this.set("orientation", false);
// By default custom-covershots is false, if user want poster they can upload it
this.set("picksnapshots", this.get("custom-covershots"));
}
if (!Info.isMobile())
this.set("orientation", false);
this.set("currentorientation", window.innerHeight > window.innerWidth ? "portrait" : "landscape");
this._screenRecorderVerifier();
},
getMediaType: function() {
return "video";
},
getCovershotFile: function() {
return this.__lastCovershotUpload;
},
getVideoFile: function() {
return this._videoFile || (this.recorder && this.recorder.localPlaybackSource()) || null;
},
_initialState: InitialState,
state: function() {
return this.host.state();
},
recorderAttached: function() {
return !!this.recorder;
},
videoError: function() {
return this.__error;
},
_error: function(error_type, error_code) {
this.__error = {
error_type: error_type,
error_code: error_code
};
this.trigger("error:" + error_type, error_code);
this.trigger("error", error_type, error_code);
},
_clearError: function() {
this.__error = null;
},
_detachRecorder: function() {
if (this.recorder)
this.recorder.weakDestroy();
this.recorder = null;
this.set("hasrecorder", false);
// to prevent autorecord if user not set, but use reset()
this.set("autorecord", this.get("optionsinitialstate").autorecord);
},
_validateParameters: function() {
var fitStrategies = ["crop", "pad", "original"];
if (!fitStrategies.includes(this.get("videofitstrategy"))) {
console.warn("Invalid value for videofitstrategy: " + this.get("videofitstrategy") + "\nPossible values are: " + fitStrategies.slice(0, -1).join(", ") + " or " + fitStrategies.slice(-1));
}
if (!fitStrategies.includes(this.get("posterfitstrategy"))) {
console.warn("Invalid value for posterfitstrategy: " + this.get("posterfitstrategy") + "\nPossible values are: " + fitStrategies.slice(0, -1).join(", ") + " or " + fitStrategies.slice(-1));
}
if (this.get("stretch") || this.get("stretchwidth") || this.get("stretchheight")) {
console.warn("Stretch parameters were deprecated, your player will stretch to the full container width by default.");
}
var deprecatedCSS = ["minheight", "minwidth", "minheight", "minwidth"];
deprecatedCSS.forEach(function(parameter) {
if (this.get(parameter)) console.warn(parameter + " parameter was deprecated, please use CSS instead.");
}.bind(this));
},
_videoRecorderWrapperOptions: function() {
var _screen = null;
var _resizeMode = this.get("resizemode");
if ((this.get("allowscreen") && this.get("record_media") === "screen") || (this.get("allowmultistreams") && this.get("record_media") === "multistream")) {
_screen = this.get("screen");
if (!_resizeMode) {
_resizeMode = 'none';
}
}
if (!this.get("allowrecord") && (this.get("autorecord") || this.get("skipinitial"))) {
if (this.get("allowscreen") || this.get("allowmultistreams")) {
this.set("record_media", this.get("allowscreen") ? "screen" : "multistream");
_screen = {};
}
}
return {
simulate: this.get("simulate"),
recordVideo: !this.get("onlyaudio"),
screenResizeMode: this.get("screenresizemode"),
recordAudio: !this.get("noaudio"),
recordingWidth: this.get("nativeRecordingWidth"),
recordingHeight: this.get("nativeRecordingHeight"),
audioBitrate: typeof this.get("audiobitrate") === "number" ? this.get("audiobitrate") : undefined,
videoBitrate: typeof this.get("videobitrate") === "number" ? this.get("videobitrate") : undefined,
webrtcStreaming: !!this.get("webrtcstreaming"),
webrtcStreamingIfNecessary: !!this.get("webrtcstreamingifnecessary"),
// webrtcOnMobile: !!this.get("webrtconmobile"),
localPlaybackRequested: this.get("localplayback"),
screen: _screen,
resizeMode: _resizeMode,
framerate: this.get("framerate"),
flip: this.get("flip-camera"),
flipscreen: this.get("flipscreen"),
fittodimensions: this.get("fittodimensions"),
cpuFriendly: this.get("cpu-friendly")
};
},
_attachRecorder: function() {
if (this.recorderAttached())
return;
if (!this.__activated) {
this.__attachRequested = true;
return;
}
if (this.get("record_media") === "screen" && typeof navigator.mediaDevices !== 'undefined')
if (typeof navigator.mediaDevices.getDisplayMedia === "undefined")
this.set("webrtcstreaming", true);
this.set("hasrecorder", true);
this.__attachRequested = false;
var video = this.activeElement().querySelector("[data-video='video']");
this._clearError();
this.recorder = VideoRecorderWrapper.create(Objs.extend({
element: video
}, this._videoRecorderWrapperOptions()));
if (this.recorder) {
this.trigger("attached");
this.set("pausable", this.get("pausable") && this.recorder.canPause());
} else
this._error("attach");
},
_softwareDependencies: function() {
if (!this.recorderAttached() || !this.recorder)
return Promise.error("No recorder attached.");
return this.recorder.softwareDependencies();
},
_bindMedia: function() {
if (this._bound || !this.recorderAttached() || !this.recorder)
return;
this.recorder.ready.success(function() {
if (!this.recorder) return;
this.recorder.on("require_display", function() {
this.set("hideoverlay", true);
}, this);
this.recorder.bindMedia().error(function(e) {
this.trigger("access_forbidden", e);
this.set("hideoverlay", false);
this.off("require_display", null, this);
this._error("bind", e);
}, this).success(function() {
if (!this.recorder) return;
this.trigger("access_granted");
this.recorder.setVolumeGain(this.get("microphone-volume"));
this.set("hideoverlay", false);
this.off("require_display", null, this);
this.recorder.once("mainvideostreamended", function() {
this.trigger("mainvideostreamended");
}, this);
this.recorder.enumerateDevices().success(function(devices) {
if (!this.recorder) return;
this.recorder.once("currentdevicesdetected", function(currentDevices) {
this.set("selectedcamera", currentDevices.video);
this.set("selectedmicrophone", currentDevices.audio);
if (this.get("outsourceSelectors").length > 0) {
this.setOutsourceSelectors(currentDevices);
}
}, this);
this.set("cameras", new Collection(Objs.values(devices.video)));
this.set("microphones", new Collection(Objs.values(devices.audio)));
this.trigger(Types.is_empty(devices.video) ? "no_camera" : "has_camera");
this.trigger(Types.is_empty(devices.audio) ? "no_microphone" : "has_microphone");
this.set("showaddstreambutton", this._showAddStreamButton());
}, this);
if (!this.get("noaudio"))
this.recorder.testSoundLevel(true);
this.set("devicetesting", true);
while (this.get("snapshots").length > 0) {
var snapshot = this.get("snapshots").unshift();
this.recorder.removeSnapshot(snapshot);
}
this._bound = true;
this.trigger("bound");
}, this);
}, this);
},
isWebrtcStreaming: function() {
return this.recorder && this.recorder.isWebrtcStreaming();
},
_showAddStreamButton: function() {
return this.get("allowmultistreams") && (this.get("cameras").count() > 1 || this.get("cameras").count() >= 1 && ((this.get("record_media") !== "screen" || (this.get("record_media") !== "multistream"))));
},
_initSettings: function() {
// Without below line re-recorder will not launch
this.set("snapshots", []);
this.thumbnails = [];
this.__lastCovershotUpload = undefined;
this.__pauseDelta = 0;
this.set("starttime", undefined);
this.set("endtime", undefined);
this.set("duration", 0);
this.set("videometadata", {
"height": null,
"width": null,
"ratio": null,
"thumbnails": {
"mainimage": null,
"images": []
},
"placeholderSnapshot": null
});
},
_initializeUploader: function() {
if (this._videoUploader) this._videoUploader.weakDestroy();
if (this._dataUploader) this._dataUploader.weakDestroy();
this._dataUploader = new MultiUploader();
},
_unbindMedia: function() {
if (!this._bound)
return;
this.recorder.unbindMedia();
this._bound = false;
},
_uploadCovershot: function(image) {
if (this.get("simulate"))
return;
this.__lastCovershotUpload = image;
var uploader = this.recorder ?
this.recorder.createSnapshotUploader(image, this.get("snapshottype"), this.get("uploadoptions").image) :
RecorderSupport.createSnapshotUploader(image, this.get("snapshottype"), this.get("uploadoptions").image);
uploader.upload();
this._dataUploader.addUploader(uploader);
},
_uploadCovershotFile: function(file) {
if (this.get("simulate"))
return;
this.__lastCovershotUpload = file;
var uploader = FileUploader.create(Objs.extend({
source: file
}, this.get("uploadoptions").image));
uploader.upload();
this._dataUploader.addUploader(uploader);
},
/**
* Upload single image Blob file with thumbnails to the server
* @param {Blob} file
* @private
*/
_uploadThumbnails: function(file) {
if (this.get("simulate"))
return;
this.set("videometadata", Objs.tree_merge(this.get("videometadata"), {
thumbnails: {
mainimage: file
}
}));
var uploader = FileUploader.create(Objs.extend({
source: file
}, this.get("uploadoptions").thumbnail));
uploader.upload();
this._dataUploader.addUploader(uploader);
},
/**
* Upload VTT Blob file to the server with all details about thumbnails
* @param {Blob} file
* @private
*/
_uploadThumbnailTracks: function(file) {
if (this.get("simulate"))
return;
var uploader = FileUploader.create(Objs.extend({
source: file
}, this.get("uploadoptions").tracks));
// Add Thumbnails as Track element to the player
if (this.get("uploadoptions").tracks.url) {
this.get("tracktags").push({
kind: 'thumbnails',
src: this.get("uploadoptions").tracks.url
});
}
uploader.upload();
this._dataUploader.addUploader(uploader);
},
_uploadVideoFile: function(file) {
if (this.get("simulate"))
return;
var uploader = FileUploader.create(Objs.extend({
source: file
}, this.get("uploadoptions").video));
uploader.upload();
this._videoUploader = uploader;
this._dataUploader.addUploader(uploader);
},
_prepareRecording: function() {
return Promise.create(true);
},
_startRecording: function() {
if (this.__recording)
return Promise.error(true);
if (!this.get("noaudio"))
this.recorder.testSoundLevel(false);
this.set("devicetesting", false);
return this.recorder.startRecord({
video: this.get("uploadoptions").video,
audio: this.get("uploadoptions").audio,
webrtcStreaming: this.get("uploadoptions").webrtcStreaming
}).success(function() {
this.__recording = true;
this.__recording_start_time = Time.now();
}, this);
},
_stopRecording: function() {
if (!this.__recording)
return Promise.error(true);
return this.recorder.stopRecord({
video: this.get("uploadoptions").video,
audio: this.get("uploadoptions").audio,
webrtcStreaming: this.get("uploadoptions").webrtcStreaming,
noUploading: this.get("uploadoptions").noUploading
}).success(function(uploader) {
this.__recording = false;
uploader.upload();
this._dataUploader.addUploader(uploader);
}, this);
},
isRecording: function() {
return this.__recording;
},
isFormatSupported: function() {
return (this.recorder && this.recorder.supportsLocalPlayback()) || this._videoFilePlaybackable;
},
_verifyRecording: function() {
return Promise.create(true);
},
_afterActivate: function(element) {
inherited._afterActivate.call(this, element);
this.set("activated", true);
this.__activated = true;
if (this.__attachRequested)
this._attachRecorder();
this.persistentTrigger("loaded");
this.activeElement().classList.add(this.get("csscommon") + "-full-width");
},
_showBackgroundSnapshot: function() {
if (this.get("onlyaudio"))
return;
this._hideBackgroundSnapshot();
if (this.get("snapshots") && this.get("selectfirstcovershotonskip")) {
if (this.get("snapshots")[0])
this.__backgroundSnapshot = this.get("snapshots")[0];
}
if (!this.__backgroundSnapshot && this.recorder) {
this.__backgroundSnapshot = this.recorder.createSnapshot(this.get("snapshottype"));
}
var el = this.activeElement().querySelector("[data-video]");
var dimensions = Dom.elementDimensions(el);
if (this.__backgroundSnapshot) {
var _top, _left, _width, _height, _dimensions;
_top = 0;
_left = 0;
_width = dimensions.width;
_height = dimensions.height;
if (this.recorder._recorder._videoTrackSettings && typeof this.recorder._recorder._videoTrackSettings.videoInnerFrame !== "undefined") {
_dimensions = this.recorder._recorder._videoTrackSettings.videoInnerFrame;
_width = _dimensions.width || _width;
_height = _dimensions.height || _height;
_left = (dimensions.width - _width) / 2;
_top = (dimensions.height - _height) / 2;
}
this.__backgroundSnapshotDisplay = this.recorder.createSnapshotDisplay(el, this.__backgroundSnapshot, _left, _top, _width, _height);
}
},
_hideBackgroundSnapshot: function() {
if (this.get("onlyaudio"))
return;
if (this.__backgroundSnapshotDisplay)
this.recorder.removeSnapshotDisplay(this.__backgroundSnapshotDisplay);
delete this.__backgroundSnapshotDisplay;
if (this.__backgroundSnapshot)
this.recorder.removeSnapshot(this.__backgroundSnapshot);
delete this.__backgroundSnapshot;
},
_getFirstFrameSnapshot: function() {
if (this.__firstFrameSnapshot)
return Promise.value(this.__firstFrameSnapshot);
if (!(this._videoFile || (this.recorder && this.recorder.localPlaybackSource().src)))
return Promise.error("No source to get the snapshot from");
var promise = Promise.create();
var blob = this._videoFile || this.recorder.localPlaybackSource().src;
RecorderSupport.createSnapshotFromSource(URL.createObjectURL(blob), this.get("snapshottype"), 0)
.success(function(snapshot) {
this.__firstFrameSnapshot = snapshot;
promise.asyncSuccess(snapshot);
URL.revokeObjectURL(blob);
}, this)
.error(function(error) {
promise.asyncError(error);
URL.revokeObjectURL(blob);
}, this);
return promise;
},
toggleFaceOutline: function(new_status) {
if (typeof new_status === 'undefined') {
this.set("faceoutline", !this.get("faceoutline"));
} else {
this.set("faceoutline", new_status);
}
},
isMobile: function() {
return Info.isMobile();
},
object_functions: [
"record", "rerecord", "record_screen", "stop", "play", "pause", "reset", "cancel",
"pause_recorder", "resume", "upload_video", "upload_covershot", "select_camera",
"select_microphone", "add_new_stream", "trim", "toggle_face_mode"
],
functions: {
cancel: function() {
if (confirm(this.stringUnicode("cancel-confirm")))
this.execute("reset");
},
record: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("record");
return;
}
this.host.state().record();
},
record_video: function() {
this.host.state().selectRecord();
},
record_screen: function(isMultiStream) {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("record_screen");
return;
}
this.host.state().selectRecordScreen(isMultiStream);
},
pause_recorder: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("pause_recorder");
return;
}
if (typeof this.recorder !== 'undefined') {
this.__paused = true;
this.__pauseStart = Time.now();
this.__recording = false;
this.recorder.pauseRecord();
this.recorder._recorder.once("paused", function(ev) {
this.set("resumevisible", true);
}, this);
}
},
resume: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("resume");
return;
}
if (typeof this.recorder !== 'undefined')
this._resume();
},
video_file_selected: function(file) {
this.__selected_video_file = file;
if (!this.get("manual-upload"))
this.execute("upload_video");
},
upload_video: function(file) {
this.host.state().selectUpload(file || this.__selected_video_file);
},
upload_covershot: function(file) {
this.host.state().uploadCovershot(file);
},
select_camera: function(camera_id) {
if (this.recorder) {
this.recorder.setCurrentDevices({
video: camera_id
});
this.set("showaddstreambutton", this._showAddStreamButton());
this.set("selectedcamera", camera_id);
}
},
select_microphone: function(microphone_id) {
if (this.recorder) {
this.recorder.setCurrentDevices({
audio: microphone_id
});
this.recorder.testSoundLevel(true);
this.set("selectedmicrophone", microphone_id);
}
this.set("microphonehealthy", false);
},
toggle_face_mode: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("toggle_face_mode");
return;
}
this.toggleFaceMode();
},
add_new_stream: function(deviceId) {
this._add_new_stream(deviceId);
},
invoke_skip: function() {
this.trigger("invoke-skip");
},
select_image: function(image) {
this.trigger("select-image", image);
},
rerecord: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("rerecord");
return;
}
if (confirm(this.stringUnicode("rerecord-confirm"))) {
this.host.state().rerecord();
this._initSettings();
}
},
stop: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("stop");
return;
}
// If recorder is paused need resume first,
// setting this._recording to true also could be enough
if (this.__paused)
this._resume();
this.host.state().stop();
},
play: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("play");
return;
}
this.host.state().play();
},
pause: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("pause");
return;
}
this.host.state().pause();
},
message_click: function() {
this.trigger("message-click");
},
message_link_click: function(link) {
this.trigger("message-link-click", link);
},
playing: function() {
this.trigger("playing");
},
paused: function() {
this.trigger("paused");
},
ended: function() {
this.trigger("ended");
},
reset: function() {
if (this._delegatedRecorder) {
this._delegatedRecorder.execute("reset");
return;
}
this._stopRecording().callback(function() {
this._unbindMedia();
this._hideBackgroundSnapshot();
this._detachRecorder();
this._initSettings();
this.host.state().next("Initial");
}, this);
},
trim: function(start, end) {
if (this.host.state().state_name() !== "Trimming") return;
this.trigger("manual-trim", start, end);
},
manual_submit: function() {
this.set("rerecordable", false);
this.set("manualsubmit", false);
this.trigger("manually_submitted");
},
ready_to_play: function() {
this.trigger("ready_to_play");
}
},
_resume: function() {
this.__paused = false;
this.__pauseDelta += Time.now() - this.__pauseStart;
this.__recording = true;
this.recorder.resumeRecord();
this.recorder._recorder.once("resumed", function() {
this.set("resumevisible", false);
}, this);
},
destroy: function() {
this._timer.destroy();
this.host.destroy();
this._detachRecorder();
inherited.destroy.call(this);
},
deltaCoefficient: function() {
return this.recorderAttached() ? this.recorder.deltaCoefficient() : null;
},
blankLevel: function() {
return this.recorderAttached() ? this.recorder.blankLevel() : null;
},
lightLevel: function() {
return this.recorderAttached() ? this.recorder.lightLevel() : null;
},
soundLevel: function() {
return this.recorderAttached() ? this.recorder.soundLevel() : null;
},
toggleFaceMode: function() {
if (this.recorder) {
this.recorder.setCameraFace(this.get("camerafacefront"));
this.set("camerafacefront", !this.get("camerafacefront"));
}
},
_timerFire: function() {
if (this.destroyed())
return;
this.set("currentorientation", window.innerHeight > window.innerWidth ? "portrait" : "landscape");
try {
if (this.recorderAttached() && this.get("devicetesting")) {
if (!this.get("onlyaudio")) {
var lightLevel = this.lightLevel();
this.set("camerahealthy", lightLevel >= 100 && lightLevel <= 200);
}
if (!this.get("noaudio") && !this.get("microphonehealthy") && this.soundLevel() >= 1.01) {
this.set("microphonehealthy", true);
this.recorder.testSoundLevel(false);
}
}
} catch (e) {}
if (!this.get("onlyaudio") && this.__recording) {
if (this.get("picksnapshots")) {
if (this.__recording_start_time + 500 < Time.now()) {
var p = this.get("snapshots").length < this.get("snapshotmax") ? 0.25 : 0.05;
if (Math.random() <= p) {
var snap = this.recorder.createSnapshot(this.get("snapshottype"));
if (snap) {
if (!this.get('videometadata').height && typeof Image !== 'undefined' && this.get("createthumbnails")) {
RecorderSupport.snapshotMetaData(snap).success(function(data) {
var _thumbWidth = data.orientation === 'landscape' ? 80 : 35;
this.set("videometadata", Objs.tree_merge(this.get("videometadata"), data));
this.set("videometadata", Objs.tree_merge(this.get("videometadata"), {
"thumbnails": {
width: _thumbWidth,
height: Math.floor(_thumbWidth / data.width * data.height)
}
}));
}, this);
}
if (this.get("snapshots").length < this.get("snapshotmax")) {
this.get("snapshots").push(snap);
} else {
var i = Math.floor(Math.random() * this.get("snapshotmax"));
this.recorder.removeSnapshot(this.get("snapshots")[i]);
this.get("snapshots")[i] = snap;
}
if (this.get("createthumbnails")) {
var _currentRecordingSecond = Math.floor((Time.now() - this.__recording_start_time) / 1000);
var _thumbLatestIndex = this.get("videometadata").thumbnails.images.length > 1 ? this.get("videometadata").thumbnails.images.length - 1 : 0;
var _latestThumb = this.get("videometadata").thumbnails.images[_thumbLatestIndex];
// Add thumb each 2 seconds
if (typeof _latestThumb !== 'undefined') {
if (_currentRecordingSecond > _latestThumb.time + 1) {
this.get("videometadata").thumbnails.images.push({
time: _currentRecordingSecond,
snap: snap
});
}
} else {
this.get("videometadata").thumbnails.images.push({
time: _currentRecordingSecond,
snap: snap
});
}
}
}
}
}
} else {
// took snap only one for background view
if (!this.get("videometadata").processingPlaceholder) {
this.get("videometadata").processingPlaceholder = this.recorder.createSnapshot(this.get("snapshottype"));
}
}
}
try {
if (this.recorderAttached() && this._timer.fire_count() % 20 === 0 && this._accessing_camera) {
var signal = this.blankLevel() >= 0.01;
if (signal !== this.__cameraSignal) {
this.__cameraSignal = signal;
this.trigger(signal ? "camera_signal" : "camera_nosignal");
}
}
if (this.recorderAttached() && this._timer.fire_count() % 20 === 10 && this._accessing_camera) {
var delta = this.recorder.deltaCoefficient();
var responsive = delta === null || delta >= 0.5;
if (responsive !== this.__cameraResponsive) {
this.__cameraResponsive = responsive;
this.trigger(responsive ? "camera_responsive" : "camera_unresponsive");
}
}
} catch (e) {}
this._updateCSSSize();
},
domDimensions: function() {
return Dom.elementDimensions(this.activeElement().childNodes[0]);
},
_updateCSSSize: function() {
var width = this.domDimensions().width;
this.set("csssize", width > 400 ? "normal" : (width > 300 ? "medium" : "small"));
},
videoHeight: function() {
var _clientHeight = (window.innerHeight || document.body.clientHeight);
if (!this.recorderAttached())
return _clientHeight;
else {
var _height = this.recorder.cameraHeight();
return _height > _clientHeight ? _clientHeight : _height;
}
},
videoWidth: function() {
var _clientWidth = (window.innerWidth || document.body.clientWidth);
if (!this.recorderAttached())
return _clientWidth;
else {
var _width = this.recorder.cameraWidth();
return _width > _clientWidth ? _clientWidth : _width;
}
},
aspectRatio: function() {
var width = this.videoWidth();
var height = this.videoHeight();
return width / height;
},
isPortrait: function() {
if (typeof this.recorder !== "undefined") {
if (typeof this.recorder._recorder._videoTrackSettings !== "undefined") {
return this.recorder._recorder._videoTrackSettings.aspectRatio < 1.0;
}
}
return this.aspectRatio() < 1.00;
},
isLandscape: function() {
return !this.isPortrait();
},
parentWidth: function() {
return this.get("width") || Dom.elementDimensions(this.activeElement()).width;
},
parentHeight: function() {
return this.get("height") || Dom.elementDimensions(this.activeElement()).height;
},
parentAspectRatio: function() {
return this.parentWidth() / this.parentHeight();
},
averageFrameRate: function() {
return this.recorderAttached() ? this.recorder.averageFrameRate() : null;
},
cloneAttrs: function() {
return Objs.map(this.attrs, function(value, key) {
return this.get(key);
}, this);
},
popupAttrs: function() {
return {
popup: false,
width: this.get("popup-width"),
height: this.get("popup-height")
};
},
imageUploaded: function() {
return !!this.__lastCovershotUpload;
},
audioUploaded: function() {
return this.recorder && this.recorder.localPlaybackSource() && !!this.recorder.localPlaybackSource().audiosrc;
},
/**
*
* @param {boolean =} setFalse
* @private
*/
_screenRecorderVerifier: function(setFalse) {
if (setFalse) {
this.set("allowscreen", false);
this.set("allowmultistreams", false);
}
if ((this.get("allowscreen") || this.get("allowmultistreams")) && this.get("screenrecordmandatory") && !Info.isScreenRecorderSupported()) {
this.get("initialmessages").push({
id: typeof Date.now !== 'undefined' ? +Date.now() : 1498744,
message: this.string("screen-recorder-is-not-supported"),
type: 'warning'
});
}
},
/**
* Will add new stream based on provided ID
* @param deviceId
*/
_add_new_stream: function(deviceId) {
var _selected;
var _currentTracks = this.recorder._recorder.stream().getTracks();
this.get("cameras").iterate(function(videoDevice) {
var _videoDevice = videoDevice.data();
deviceId = deviceId || _videoDevice.id; // In case if argument is empty take any video source
if (!_selected && deviceId === _videoDevice.id) {
this.set("loadlabel", this.string("adding-new-stream"));
this.set("loader_active", true);
this.recorder.addMultiStream(_videoDevice, {
positionX: this.get("addstreampositionx"),
positionY: this.get("addstreampositiony"),
width: this.get("addstreampositionwidth"),
height: this.get("addstreampositionheight")
}, _currentTracks).success(function(stream) {
_selected = true;
this.set("addstreamdeviceid", deviceId);
if (!this.get("addstreampositionheight")) {
var _height, _aspectRatio;
_aspectRatio = 1.333;
if (typeof stream.getTracks()[0] !== 'undefined') {
var _settings = stream.getTracks()[0].getSettings();
if (_settings.aspectRatio) {
_aspectRatio = _settings.aspectRatio;
} else if (_settings.height > 0 && _settings.width > 0) {
_aspectRatio = Math.round((_settings.width / _settings.height) * 100) / 100;
}
}
if (_aspectRatio)
_height = this.get("addstreampositionwidth") / _aspectRatio;
else
_height = Math.round(this.get("addstreampositionwidth") / 1.33);
this.set("addstreampositionheight", _height);
}
this.set("loadlabel", "");
this.set("loader_active", false);
this.set("showaddstreambutton", false);
if (this.get("allowmultistreams") && (this.get("multistreamreversable") || this.get("multistreamdraggable") || this.get("multistreamresizeable"))) {
this.set("helperframe_active", true);
this.set("framevisible", true);
}
}, this).error(function(message) {
console.warn(message);
this.set("loadlabel", message);
this.set("loader_active", false);
}, this);
}
}, this);
},
/**
* Will set settings on outsource elements
*/
setOutsourceSelectors: function(choosenDevices) {
Objs.map(this.get("outsourceSelectors"), function(item) {
var options = item.options;
var element = document.getElementById(item.selector);
if (element) {
var isCamera = item.isCamera;
var choosenDevice = choosenDevices[isCamera ? 'video' : 'audio'];
if ((options.type === "select" && element.tagName !== "SELECT") || (options.type !== "select" && element.tagName === "SELECT")) {
var message = "You sould provide correct element selector, option element could be only select element child";
var childNode = document.createTextNode(message);
if (element.tagName === "SELECT") {
var textNode = childNode;
childNode = document.createElement('option');
childNode.appendChild(textNode);
}
element.appendChild(childNode);
console.warn(message);
} else {
this.__buildOutsourceElement(element, item.options, isCamera, choosenDevice);
}
} else {
console.warn("There are no element with id: " + item.selector + " to implement 'outsource-selectors'");
}
}, this);
},
/**
* Just Helper funtion build outsources elements
* @param element
* @param options
* @param isCamera
* @param choosenDevice
* @private
*/
__buildOutsourceElement: function(element, options, isCamera, choosenDevice) {
var self = this;
var deviceCollections = isCamera ? this.get("cameras") : this.get("microphones");
var listeners = [];
var initialSelectorText = '';
element.disabled = deviceCollections.count() <= 1 && options.disabled;
if (deviceCollections.count() > 0) {
// Clear initial select content
if (options.type === "select" && element.options) {
if (Types.is_defined(element.options[0]))
initialSelectorText = element.options[0].innerText;
while (element.options.length > 0) {
element.remove(0);
}
}
deviceCollections.iterate(function(device) {
var _details = '';
if (options.showCapabilities && isCamera) {
var capabilities = device.get("capabilities");
if (capabilities) {
if (typeof capabilities.width !== "undefined" && typeof capabilities.height !== "undefined") {
_details = '(' + capabilities.width.max + 'x' + capabilities.height.max + ')';
}
}
}
var id = device.get("id");
var label = device.get("label");
if (options.type === "select" && element.tagName === "SELECT") {
var option = document.createElement('option');
option.value = id;
option.innerText = label + _details;
option.selected = id === choosenDevice;
if (options.className) option.className += options.className;
element.appendChild(option);
}
if (options.type === "radio") {
var radioInput = document.createElement('input');
var radioInputLabel = document.createElement('label');
radioInput.name = self.get("css") + (isCamera ? '-cam' : '-mic') + '-radio-input';
radioInput.type = 'radio';
if (options.className) {
radioInputLabel.className += options.className;
}
radioInputLabel.setAttribute('for', this.get("css") + id);
radioInput.value = id;
radioInput.checked = id === choosenDevice;
// radioInputLabel.innerText = label + _details;
var radioInputTextNode = document.createTextNode(label + _details);
radioInputLabel.appendChild(radioInput);
radioInputLabel.appendChild(radioInputTextNode);
element.appendChild(radioInputLabel);
// Add event listener on change
radioInput.addEventListener("change", function(ev) {
var _radio = ev.target;
var value = _radio.value;
if (_radio.checked && value) {
if (isCamera) {
self.select_camera(value);
} else {
self.select_microphone(value);
}
}
});
listeners.push({
type: "change",
element: radioInput,
container: radioInputLabel
});
}
}, this);
// Add event listener on change
if (options.type === "select") {
listeners.push({
type: "change",
element: element
});
element.addEventListener("change", function(ev) {
Array.from(ev.target.options).forEach(function(option) {
var value = option.value;
if (option.selected && value) {
if (isCamera) {
self.select_camera(value);
} else {
self.select_microphone(value);
}
}
});
});
}
self.on("recording", function() {
if (listeners.length > 0) {
Objs.map(listeners, function(listener) {
var type = listener.type;
var el = listener.element;
var container = listener.container || el;
removeEventListener(type, el);
if (!Types.is_undefined(container.options)) {
while (container.options.length > 0) {
container.remove(0);
}
var option = document.createElement('option');
var textNode = document.createTextNode(initialSelectorText);
option.appendChild(textNode);
container.appendChild(option);
} else {
container.parentNode.removeChild(container);
}
});
}
});
}
}
};
}], {
recorderStates: function() {
return [RecorderStates];
}
})
.register("ba-videorecorder")
.registerFunctions({
/*<%= template_function_cache(dirname + '/recorder.html') %>*/
})
.attachStringTable(Assets.strings)
.addStrings({
"recorder-error": "An error occurred, please try again later. Click to retry.",
"attach-error": "We could not access the media interface. Depending on the device and browser, you might need to access the page via SSL.",
"software-required": "Please click below to install / activate the following requirements in order to proceed.",
"software-waiting": "Waiting for the requirements to be installed / activated. You might need to refresh the page after completion.",
"access-forbidden": "Access to the media was forbidden. Click to retry.",
"pick-covershot": "Pick a covershot.",
"pick-covershot-frame": "Select a frame to use as covershot.",
"framerate-warning": "The video frame rate is very low. We recommend closing all other programs and browser tabs or to use a faster computer.",
"uploading": "Uploading",
"uploading-src-error": "Unable to play back video now, uploading is still in progress",
"uploading-failed": "Uploading failed - click here to retry.",
"upload-error-duration": "Length of the uploaded video does not meet the requirements - click here to retry.",
"resolution-constraint-error": "The file you've selected does not match the required resolution - click here to retry.",
"verifying": "Verifying",
"verifying-failed": "Verifying failed - click here to retry.",
"rerecord-confirm": "Do you really want to redo your video?",
"cancel-confirm": "Do you really want to cancel your video upload?",
"video_file_too_large": "Your video file is too large (%s) - click here to try again with a smaller video file.",
"unsupported_video_type": "Please upload: %s - click here to retry.",
"orientation-portrait-required": "Please rotate your device to record in portrait mode.",
"orientation-landscape-required": "Please rotate your device to record in landscape mode.",
"switch-camera": "Switch camera",
"prepare-covershot": "Preparing covershots",
"prepare-thumbnails": "Preparing seeking thumbnails",
"adding-new-stream": "Adding New Stream",
"missing-track": "Required audio or video track is missing",
"device-already-in-use": "At least one of your input devices are already in use",
"browser-permission-denied": "Permission denied by browser, please grant access and reload page",
"screen-recorder-is-not-supported": "Screen recorder is not supported on this device",
"trim-prompt": "Do you want to trim your video?",
"trim-video": "Move the start and end markers to trim your video",
"wait-for-trim": "Waiting for trim command...",
"supported-mode": "Media resolution should be in '%s' mode.",
"re-choose-action": "Please click to choose another input device or retry action."
});
});