Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import cloudinary from './cloudinary';
import cloudinaryAnalytics from './cloudinary-analytics';
import contextMenu from './context-menu';
import floatingPlayer from './floating-player';
import mobileTouchControls from './mobile-touch-controls';
import sourceSwitcher from './source-switcher';
import styledTextTracks from './styled-text-tracks';
import vttThumbnails from './vtt-thumbnails';
Expand All @@ -31,6 +32,7 @@ const plugins = {
cloudinaryAnalytics,
contextMenu,
floatingPlayer,
mobileTouchControls,
sourceSwitcher,
styledTextTracks,
vttThumbnails,
Expand Down
123 changes: 123 additions & 0 deletions src/plugins/mobile-touch-controls/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import videojs from 'video.js';
import './mobile-touch-controls.scss';

const mobileTouchControls = (options, player) => {
if (!(videojs.browser.IS_IOS || videojs.browser.IS_ANDROID ||
/Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))) {
return {};
}

let isInteractionActive = false;
let inactivityHandler = null;
let pauseHandler = null;
let pauseButton = null;
let originalHandleClick = null;

const removePauseHandler = () => {
if (!pauseHandler) return;
pauseButton?.removeEventListener('click', pauseHandler, { capture: true });
pauseButton?.removeEventListener('touchend', pauseHandler, { capture: true });

const component = player.getChild('bigPlayButton');
if (component && originalHandleClick) component.handleClick = originalHandleClick;

pauseHandler = pauseButton = originalHandleClick = null;
};

const updateTouchState = () => {
if (!player.paused()) {
player.addClass('cld-mobile-touch-playing');
removePauseHandler();

const btn = player.el().querySelector('.vjs-big-play-button');
if (!btn) return;

pauseHandler = (e) => {
e.preventDefault();
e.stopImmediatePropagation();
if (!player.paused()) {
player.pause();
setTimeout(() => {
updateTouchState();
if (player.hasClass('cld-mobile-touch-active')) {
player.setTimeout(() => player.removeClass('cld-mobile-touch-active'), 2000);
}
}, 50);
}
return false;
};

btn.addEventListener('click', pauseHandler, { capture: true });
btn.addEventListener('touchend', pauseHandler, { capture: true });

const component = player.getChild('bigPlayButton');
if (component) {
originalHandleClick = component.handleClick;
component.handleClick = () => pauseHandler({ preventDefault: () => {}, stopImmediatePropagation: () => {} });
}
pauseButton = btn;
} else {
player.removeClass('cld-mobile-touch-playing');
removePauseHandler();
}
};

const setupInactivityHandler = () => {
if (inactivityHandler) player.off('userinactive', inactivityHandler);

inactivityHandler = () => {
player.removeClass('cld-mobile-touch-active');
setTimeout(() => {
if (!player.hasClass('cld-mobile-touch-active')) {
player.removeClass('cld-mobile-touch-playing');
removePauseHandler();
isInteractionActive = false;
}
if (inactivityHandler) {
player.off('userinactive', inactivityHandler);
inactivityHandler = null;
}
}, 250);
};

player.one('userinactive', inactivityHandler);
};

player.ready(() => {
const el = player.el();
if (!el) return;

const onTouch = (e) => {
if (e.target.closest('.vjs-control-bar, .vjs-menu') ||
e.target.matches('.vjs-control, .vjs-button') ||
!player.hasStarted()) return;

player.addClass('cld-mobile-touch-active');
if (!isInteractionActive) {
isInteractionActive = true;
updateTouchState();
}
setupInactivityHandler();
};

const onPlayPause = () => isInteractionActive && updateTouchState();

el.addEventListener('touchend', onTouch, { passive: true });
player.on('play', onPlayPause);
player.on('pause', onPlayPause);

player.on('dispose', () => {
el.removeEventListener('touchend', onTouch);
removePauseHandler();
if (inactivityHandler) player.off('userinactive', inactivityHandler);
player.off('play', onPlayPause);
player.off('pause', onPlayPause);
});
});

return {};
};

export default function(opts = {}) {
return mobileTouchControls(opts, this);
}
22 changes: 22 additions & 0 deletions src/plugins/mobile-touch-controls/mobile-touch-controls.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.video-js {
&.cld-mobile-touch-active .vjs-big-play-button {
display: block;
opacity: 1;
visibility: visible;
}

&.cld-mobile-touch-playing .vjs-big-play-button .vjs-icon-placeholder:before {
content: '';
position: absolute;
border: none;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) translate(-5.5px, 0);
width: 4px;
height: 22px;
background: currentColor;
border-radius: 2px;
box-shadow: 7px 0 0 0 currentColor;
}
}
5 changes: 5 additions & 0 deletions src/video-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class VideoPlayer extends Utils.mixin(Eventable) {
this._initPlugins();
this._initJumpButtons();
this._initPictureInPicture();
this._initMobileTouchControls();
this._setVideoJsListeners(ready);
}

Expand Down Expand Up @@ -406,6 +407,10 @@ class VideoPlayer extends Utils.mixin(Eventable) {
}
}

_initMobileTouchControls() {
this.videojs?.mobileTouchControls?.();
}

_initCloudinary() {
const cloudinaryConfig = this.playerOptions.cloudinary;
cloudinaryConfig.chainTarget = this;
Expand Down