// -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
//
// Copyright (C) 2016 Opera Software AS. All rights reserved.
//
// This file is an original work developed by Opera Software AS
'use strict';
class VideoHandler {
static get OBSERVER_ATTRIBUTE_FILTER() { return ['class', 'hidden']; }
static get OBSERVER_FILTERS() {
const filters = {
'attributes': true,
'childList': true,
'subtree': true,
};
if (this.OBSERVER_ATTRIBUTE_FILTER) {
filters.attributeFilter = this.OBSERVER_ATTRIBUTE_FILTER;
}
return filters;
}
static get SELECTOR_PLAYER() { return null; }
static get SELECTOR_PLAYER_SKIP_AD() { return null; }
static get SELECTOR_PLAYER_FFORWARD() { return null; }
static get SELECTOR_PLAYER_SEEK_BAR() { return null; }
static get SELECTOR_PLAYER_LIVE() { return null; }
static get SELECTOR_PLAYER_VOLUME() { return null; }
constructor() {
this.player_ = null;
this.playerObserver_ = new MutationObserver(() => this.onPlayerChange_());
this.state_ = {};
this.trackedVideo_ = null;
this.isScrubbing_ = false;
this.isPausedForScrubbing_ = false;
this.initializeActionHandlers_();
this.onCreateBound_ = this.onCreate_.bind(this);
this.onDetachBound_ = this.onDetach_.bind(this);
this.onReleaseBound_ = this.onRelease_.bind(this);
this.addListeners_();
VideoHandler.getCurrentURL = this.getCurrentURL_.bind(this);
VideoHandler.SUPPORTS_SEND_TO_PHONE = this.supportsSendToPhone_();
VideoHandler.SUPPORTS_ENHANCE_VIDEO_MODE = this.supportsEnhanceVideoMode_();
}
initializeActionHandlers_() {
// Maps action names to our own MediaSession action handlers.
this.actionHandlers_ = new Object();
this.actionHandlers_['skipad'] = () => {
this.triggerClick_(this.constructor.SELECTOR_PLAYER_SKIP_AD);
};
this.actionHandlers_['nexttrack'] = this.triggerFForwardClick_.bind(this);
this.actionHandlers_['seekto'] = this.onSeekToAction_.bind(this);
// Maps action names to IDs of our own MediaSession action handlers.
this.actionHandlerIds_ = new Object();
}
addListeners_() {
VideoHandler.Events.onCreate.addListener(this.onCreateBound_);
VideoHandler.Events.onDetach.addListener(this.onDetachBound_);
VideoHandler.Events.onRelease.addListener(this.onReleaseBound_);
}
hasCustomDuration_() {
return typeof this.getDuration_ === 'function';
}
checkCustomControls_() {
let enabledActions = [];
if (this.hasSkipAdControl_()) {
enabledActions.push('skipad');
}
if (this.hasFForwardControl_()) {
enabledActions.push('nexttrack');
}
if (!this.isLive_() && !this.isSeekBarHidden_()) {
enabledActions.push('seekto');
}
this.updateMediaSessionActions_(enabledActions);
}
getPlayer_(video) {
return this.constructor.SELECTOR_PLAYER &&
video.closest(this.constructor.SELECTOR_PLAYER);
}
getPlayerElement_(selector) {
return this.player_ && this.player_.querySelector(selector);
}
// Gets url to current video including time markers.
// Note: This implementation was made for youtube.com and Twitch.tv only.
// For other sites doublecheck if it works and override in subclasses if
// necessary.
getCurrentURL_(videoElement) {
if (this.isLive_()) {
return location.href;
}
const time = parseInt(videoElement.currentTime);
const timeRegex = /t=[\dhm]+s/;
let searchPart = location.search;
if (location.search.search(timeRegex) >= 0) {
searchPart = location.search.replace(timeRegex, `t=${time}s`);
} else {
searchPart += `${searchPart === '' ? '?' : '&'}t=${time}s`;
}
if (location.search === '') {
return location.href + searchPart;
}
return location.href.replace(location.search, searchPart);
}
supportsSendToPhone_() {
return false;
}
supportsEnhanceVideoMode_() {
return true;
}
hasSkipAdControl_() {
if (this.constructor.SELECTOR_PLAYER_SKIP_AD) {
return Boolean(
this.getPlayerElement_(this.constructor.SELECTOR_PLAYER_SKIP_AD));
}
return false;
}
hasFForwardControl_() {
if (this.constructor.SELECTOR_PLAYER_FFORWARD) {
return Boolean(
this.getPlayerElement_(this.constructor.SELECTOR_PLAYER_FFORWARD));
}
return false;
}
isSeekBarHidden_() {
if (this.constructor.SELECTOR_PLAYER_SEEK_BAR) {
return !Boolean(
this.getPlayerElement_(this.constructor.SELECTOR_PLAYER_SEEK_BAR));
}
return false;
}
isLive_() {
if (this.constructor.SELECTOR_PLAYER_LIVE) {
return Boolean(
this.getPlayerElement_(this.constructor.SELECTOR_PLAYER_LIVE));
}
return false;
}
onCreate_(video) {
const player = this.getPlayer_(video);
if (!player) {
return;
}
// The video element may have changed, so let the listeners below know.
player[VideoHandler.VIDEO_ELEMENT] = video;
if (this.player_ === player) {
return;
}
this.player_ = player;
video[VideoHandler.PLAYER_ELEMENT] = player;
this.player_.addEventListener('mousemove', () => {
const video = this.player_[VideoHandler.VIDEO_ELEMENT];
if (video) {
VideoHandler.Events.onVideoAreaOver.dispatch(video);
}
});
this.player_.addEventListener('mouseout', () => {
const video = this.player_[VideoHandler.VIDEO_ELEMENT];
if (video) {
VideoHandler.Events.onVideoAreaOut.dispatch(video);
}
});
}
onDetach_(video) {
if (this.trackedVideo_) {
if (this.trackedVideo_ === video) {
return;
}
this.onRelease_(this.trackedVideo_);
}
this.trackedVideo_ = video;
this.state_ = {};
this.track_(video);
this.updateMediaSessionActions_(['seekto']);
}
onPlayerChange_() { }
onRelease_(video) {
if (!video || this.trackedVideo_ !== video) {
return;
}
this.updateMediaSessionActions_([]);
this.trackedVideo_ = null;
this.untrack_(video);
}
track_() {
this.player_ = this.getPlayer_(this.trackedVideo_);
if (this.player_) {
this.playerObserver_.observe(
this.player_, this.constructor.OBSERVER_FILTERS);
this.onPlayerChange_();
}
}
triggerClick_(selector) {
if (!selector) {
return false;
}
const element = this.getPlayerElement_(selector);
if (element) {
element.click();
return true;
}
return false;
}
triggerFForwardClick_() {
return this.triggerClick_(this.constructor.SELECTOR_PLAYER_FFORWARD);
}
onSeekToAction_(actionDetails) {
if (!this.isScrubbing_ && actionDetails.fastSeek) {
this.beginScrubbing_();
}
// TODO: Use HTMLMediaElement.fastSeek() when it's available.
this.trackedVideo_.currentTime = actionDetails.seekTime;
// §10 of the MediaSession spec says |fastSeek| "will be true if the action
// is being called multiple times as part of a sequence and this is not the
// last call in that sequence."
if (this.isScrubbing_ && !actionDetails.fastSeek) {
this.endScrubbing_();
}
}
beginScrubbing_() {
if (!this.trackedVideo_.paused) {
this.trackedVideo_.pause();
this.isPausedForScrubbing_ = true;
}
this.isScrubbing_ = true;
}
endScrubbing_() {
if (this.isPausedForScrubbing_) {
this.trackedVideo_.play();
this.isPausedForScrubbing_ = false;
}
this.isScrubbing_ = false;
}
untrack_(video) {
this.playerObserver_.disconnect();
}
// Make sure our handlers for actions specified in |actions| are enabled,
// removing any action handlers set by us previously for actions that are not
// in |actions|.
updateMediaSessionActions_(actions) {
for (const action in this.actionHandlers_) {
if (actions.includes(action)) {
this.maybeEnableMediaSessionActionHandler_(action);
} else {
this.maybeDisableMediaSessionActionHandler_(action);
}
}
}
// Set up our custom action handler iff we know the page doesn't provide its
// own handler.
maybeEnableMediaSessionActionHandler_(action) {
const currentHandlerId = opr.detachedVideoPrivate.getActionHandlerId(
navigator.mediaSession, action);
if (currentHandlerId === this.actionHandlerIds_[action]) {
// Already enabled.
return;
}
const pageHandlesAction = currentHandlerId !== 0;
if (!pageHandlesAction) {
navigator.mediaSession.setActionHandler(
action, this.actionHandlers_[action]);
const newHandlerId = opr.detachedVideoPrivate.getActionHandlerId(
navigator.mediaSession, action);
this.actionHandlerIds_[action] = newHandlerId;
}
}
maybeDisableMediaSessionActionHandler_(action) {
if (!this.actionHandlerIds_[action]) {
// Already disabled.
return;
}
const currentHandlerId = opr.detachedVideoPrivate.getActionHandlerId(
navigator.mediaSession, action);
// The page may have overwritten our handler, that's fine.
if (this.actionHandlerIds_[action] === currentHandlerId) {
navigator.mediaSession.setActionHandler(action, null);
}
this.actionHandlerIds_[action] = null;
}
}
VideoHandler.PLAYER_ELEMENT = Symbol();
VideoHandler.VIDEO_ELEMENT = Symbol();
VideoHandler.Events = {
onCreate: opr.detachedVideoPrivate.onCreate,
onDetach: opr.detachedVideoPrivate.onDetach,
onRelease: opr.detachedVideoPrivate.onRelease,
onVideoAreaOut: opr.detachedVideoPrivate.onVideoAreaOut,
onVideoAreaOver: opr.detachedVideoPrivate.onVideoAreaOver,
};