static get ATTRIBUTE_CONTROLS() { return 'operadetachedviewcontrols'; }
static get ATTRIBUTE_TITLE() { return 'operadetachedviewtitle'; }
static get CONTROL_FFORWARD() { return 'fforward'; }
static get CONTROL_SUBTITLES() { return 'subtitles'; }
static get CONTROL_VOLUME() { return 'volume'; }
static get CONTROL_VOLUME_BAR() { return 'volume-bar'; }
static get CUSTOMIZATION_FFORWARD() { return 'fforward'; }
static get CUSTOMIZATION_NO_SEEK() { return 'no-seek'; }
static get CUSTOMIZATION_NO_TIME() { return 'no-time'; }
static get CUSTOMIZATION_DEFAULT() { return []; }
static get IS_PLAYER_AREA_ACTIVATES_DETACH_BUTTON() { return true; }
static get OBSERVER_ATTRIBUTE_FILTER() { return ['class', 'hidden']; }
static get OBSERVER_FILTERS() {
if (this.OBSERVER_ATTRIBUTE_FILTER) {
filters.attributeFilter = this.OBSERVER_ATTRIBUTE_FILTER;
static get SELECTOR_PLAYER() { return 'html'; }
static get SELECTOR_PLAYER_FFORWARD() { return null; }
static get SELECTOR_PLAYER_LIVE() { return null; }
static get SELECTOR_PLAYER_MUTE() { return null; }
static get SELECTOR_PLAYER_SUBTITLES() { return null; }
static get SELECTOR_PLAYER_SUBTITLES_TEXT() { return null; }
static get SELECTOR_PLAYER_TITLE() { return null; }
static get SELECTOR_PLAYER_VOLUME() { return null; }
static get SELECTOR_VIDEO() { return 'video'; }
this.playerObserver_ = new MutationObserver(() => this.onPlayerChange_());
this.trackingVideo_ = null;
this.onControlClickBound_ = this.onControlClick_.bind(this);
this.onCreateBound_ = this.onCreate_.bind(this);
this.onDetachBound_ = this.onDetach_.bind(this);
this.onReleaseBound_ = this.onRelease_.bind(this);
this.onVideoDetachedControlClickBound_ =
this.onVideoDetachedControlClick_.bind(this);
VideoHandler.Events.onControlClick.addListener(this.onControlClickBound_);
if (this.constructor.IS_PLAYER_AREA_ACTIVATES_DETACH_BUTTON) {
VideoHandler.Events.onCreate.addListener(this.onCreateBound_);
VideoHandler.Events.onDetach.addListener(this.onDetachBound_);
VideoHandler.Events.onRelease.addListener(this.onReleaseBound_);
VideoHandler.Events.onDetachHotKeyPressed.addListener(
() => this.onHotKeyPressed_());
VideoHandler.Events.onTogglePlayPausePressed.addListener(
() => this.onTogglePlayPausePressed_());
return typeof this.getDuration_ === 'function';
const customControls = this.constructor.CUSTOMIZATION_DEFAULT;
if (this.hasFForwardControl_()) {
customControls.push(this.constructor.CUSTOMIZATION_FFORWARD);
customControls.push(this.constructor.CUSTOMIZATION_NO_SEEK);
customControls.push(this.constructor.CUSTOMIZATION_NO_TIME);
} else if (this.hasCustomDuration_()) {
customControls.push(this.constructor.CUSTOMIZATION_NO_SEEK);
customControls.push(`duration:${parseInt(this.getDuration_())}`);
const controls = customControls.join(',');
if (controls !== this.state_.controls) {
this.state_.controls = controls;
this.trackingVideo_.setAttribute(
this.constructor.ATTRIBUTE_CONTROLS, controls);
this.trackingVideo_.removeAttribute(
this.constructor.ATTRIBUTE_CONTROLS);
if (this.constructor.SELECTOR_PLAYER_SUBTITLES) {
this.getPlayerElement_(this.constructor.SELECTOR_PLAYER_SUBTITLES);
let subtitlesElement = this.getPlayerElement_(
this.constructor.SELECTOR_PLAYER_SUBTITLES_TEXT);
subtitlesElement = subtitlesElement.cloneNode(true);
Array.from(subtitlesElement.querySelectorAll('br')).forEach(el => {
el.parentElement.replaceChild(document.createTextNode('\n'), el);
subtitles = subtitlesElement.textContent;
if (subtitles !== this.state_.subtitles) {
this.state_.subtitles = subtitles;
opr.detachedVideoPrivate.updateDetachedViewSubtitle(
this.trackingVideo_, subtitles);
if (!this.constructor.SELECTOR_PLAYER_TITLE) {
document.querySelector(this.constructor.SELECTOR_PLAYER_TITLE);
title = titleElement.textContent.trim();
if (title !== this.state_.title) {
this.state_.title = title;
this.trackingVideo_.setAttribute(
this.constructor.ATTRIBUTE_TITLE, title);
this.trackingVideo_.removeAttribute(this.constructor.ATTRIBUTE_TITLE);
getPlayer_(video) { return video.closest(this.constructor.SELECTOR_PLAYER); }
getPlayerElement_(selector) {
return this.player_ && this.player_.querySelector(selector);
if (this.constructor.SELECTOR_PLAYER_FFORWARD) {
this.getPlayerElement_(this.constructor.SELECTOR_PLAYER_FFORWARD));
if (this.constructor.SELECTOR_PLAYER_LIVE) {
this.getPlayerElement_(this.constructor.SELECTOR_PLAYER_LIVE));
onControlClick_({ video, control, detail }) {
if (this.trackingVideo_ !== video) {
if (control === this.constructor.CONTROL_FFORWARD) {
this.triggerFForwardClick_();
} else if (control === this.constructor.CONTROL_VOLUME) {
this.triggerClick_(this.constructor.SELECTOR_PLAYER_MUTE);
} else if (control === this.constructor.CONTROL_VOLUME_BAR) {
this.triggerSliderClick_(
this.constructor.SELECTOR_PLAYER_VOLUME, parseFloat(detail[0]));
const player = this.getPlayer_(video);
if (!player || this.player_ === player) {
video[VideoHandler.PLAYER_ELEMENT] = player;
this.player_.addEventListener('mousemove', () => {
VideoHandler.Events.onVideoAreaOver.dispatch(video);
this.player_.addEventListener('mouseout', () => {
VideoHandler.Events.onVideoAreaOut.dispatch(video);
if (!video || !this.getPlayer_(video)) {
if (this.trackingVideo_) {
if (this.trackingVideo_ === video) {
this.onRelease_(this.trackingVideo_);
this.trackingVideo_ = video;
'operadetachedviewcontrol', this.onVideoDetachedControlClickBound_);
if (!video || this.trackingVideo_ !== video) {
this.trackingVideo_ = null;
this.player_ = this.getPlayer_(this.trackingVideo_);
this.playerObserver_.observe(
this.player_, this.constructor.OBSERVER_FILTERS);
triggerClick_(selector) {
const element = this.getPlayerElement_(selector);
triggerFForwardClick_() {
return this.triggerClick_(this.constructor.SELECTOR_PLAYER_FFORWARD);
triggerSliderClick_(selector, position) {
let slider = this.getPlayerElement_(selector);
let client_rect = slider.getBoundingClientRect();
if (client_rect.width === 0 || client_rect.height === 0) {
} else if (position > 1) {
if (client_rect.width > client_rect.height) {
mouse_x = client_rect.left + client_rect.width * position;
mouse_y = client_rect.top + client_rect.height / 2;
mouse_x = client_rect.left + client_rect.width / 2;
mouse_y = client_rect.top + client_rect.height * position;
slider.dispatchEvent(new MouseEvent('click', event_init));
onVideoDetachedControlClick_(e) {
if (e.isTrusted && opr.detachedVideoPrivate.hasDetachedView(video)) {
const detail = e.controlName.split(',');
const control = detail.shift();
VideoHandler.Events.onControlClick.dispatch({ video, control, detail });
chrome.runtime.sendMessage('isactivetab', result => {
if (!result || !result.isActive) {
const videos = Array.from(document.querySelectorAll('video'));
const plaing_video = videos.find(video => !video.paused);
if (opr.detachedVideoPrivate.hasDetachedView(plaing_video)) {
opr.detachedVideoPrivate.releaseDetachedView(plaing_video);
opr.detachedVideoPrivate.trustedRequestDetachedView(plaing_video);
onTogglePlayPausePressed_() {
chrome.runtime.sendMessage('isactivetab', result => {
if (!result || !result.isActive ||
!opr.detachedVideoPrivate.hasDetachedView(this.trackingVideo_)) {
if (this.trackingVideo_.paused || this.trackingVideo_.ended)
this.trackingVideo_.play();
else if (!this.trackingVideo_.paused)
this.trackingVideo_.pause();
this.playerObserver_.disconnect();
video.removeAttribute(this.constructor.ATTRIBUTE_TITLE);
video.removeAttribute(this.constructor.ATTRIBUTE_CONTROLS);
VideoHandler.PLAYER_ELEMENT = Symbol();
onControlClick: opr.detachedVideoPrivate.onControlClick,
onCreate: opr.detachedVideoPrivate.onCreate,
onDetach: opr.detachedVideoPrivate.onDetach,
onRelease: opr.detachedVideoPrivate.onRelease,
onVideoAreaOut: opr.detachedVideoPrivate.onVideoAreaOut,
onVideoAreaOver: opr.detachedVideoPrivate.onVideoAreaOver,
onDetachHotKeyPressed: opr.detachedVideoPrivate.onDetachHotKeyPressed,
onTogglePlayPausePressed: opr.detachedVideoPrivate.onTogglePlayPausePressed,
document.addEventListener('operadetachedviewcontrol', evt => {
if (evt.isTrusted && evt.controlName === 'activatesourcetab') {
chrome.runtime.sendMessage('activatesourcetab');