Skip to content
Draft
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
3 changes: 2 additions & 1 deletion config/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export const plugins = [
'ima',
'volume-bar',
'sticky',
'hotkeys'
'hotkeys',
'mobile'
]
4 changes: 3 additions & 1 deletion examples/html5/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Vlitejs from 'vlitejs'
import VlitejsAirplay from 'vlitejs/plugins/airplay.js'
import VlitejsCast from 'vlitejs/plugins/cast.js'
import VlitejsHotkeys from 'vlitejs/plugins/hotkeys.js'
import VlitejsMobile from 'vlitejs/plugins/mobile.js'
import VlitejsPip from 'vlitejs/plugins/pip.js'
import VlitejsSubtitle from 'vlitejs/plugins/subtitle.js'
import VlitejsVolumeBar from 'vlitejs/plugins/volume-bar.js'
Expand All @@ -30,6 +31,7 @@ Vlitejs.registerPlugin('hotkeys', VlitejsHotkeys, {
seekStep: 3,
volumeStep: 0.2
})
Vlitejs.registerPlugin('mobile', VlitejsMobile)

new Vlitejs('#player', {
options: {
Expand All @@ -47,7 +49,7 @@ new Vlitejs('#player', {
muted: false,
autoHide: true
},
plugins: ['subtitle', 'pip', 'cast', 'airplay', 'volume-bar', 'hotkeys'],
plugins: ['subtitle', 'pip', 'cast', 'airplay', 'volume-bar', 'hotkeys', 'mobile'],
onReady: (player) => {
console.log(player)

Expand Down
10 changes: 7 additions & 3 deletions src/core/vlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ class Vlitejs {
if (this.type === 'video') {
this.container.addEventListener('click', this.onClickOnPlayer)
this.container.addEventListener('dblclick', this.onDoubleClickOnPlayer)
this.autoHideGranted && this.container.addEventListener('mousemove', this.onMousemove)
if (this.autoHideGranted && !this.player.isTouch) {
this.container.addEventListener('mousemove', this.onMousemove)
}
document.addEventListener(this.supportFullScreen.changeEvent, this.onChangeFullScreen)
}
}
Expand Down Expand Up @@ -269,7 +271,7 @@ class Vlitejs {
*/
stopAutoHideTimer() {
if (this.type === 'video' && this.player.elements.controlBar) {
this.player.elements.controlBar.classList.remove('v-hidden')
this.player.elements.controlBar?.classList.remove('v-hidden')
clearTimeout(this.timerAutoHide)
}
}
Expand All @@ -292,7 +294,9 @@ class Vlitejs {
if (this.type === 'video') {
this.container.removeEventListener('click', this.onClickOnPlayer)
this.container.removeEventListener('dblclick', this.onDoubleClickOnPlayer)
this.autoHideGranted && this.container.removeEventListener('mousemove', this.onMousemove)
if (this.autoHideGranted && !this.player.isTouch) {
this.container.removeEventListener('mousemove', this.onMousemove)
}
window.removeEventListener(this.supportFullScreen.changeEvent, this.onChangeFullScreen)
}
}
Expand Down
46 changes: 46 additions & 0 deletions src/plugins/mobile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Plugin: Mobile

Adds mobile-specific behaviors for touch devices.

## Overview

| <!-- --> | <!-- --> |
| ---------------- | -------------------------------------------- |
| Name | `mobile` |
| Path | `vlitejs/plugins/mobile` |
| Entry point | `vlitejs/plugins/mobile/mobile.js` |
| Provider&sup2; | `'html5', 'youtube', 'vimeo', 'dailymotion'` |
| Media type&sup3; | `'video'` |

The plugin is automatically **no-op on non-touch devices**.
It relies on the internal `player.isTouch` flag, so desktop behavior is not affected.

### Behavior on touch devices

On touch devices only:

- A **tap on the overlay** (`.v-overlay`) behaves as:
- **First tap** (control bar hidden): shows the control bar
- **Next taps** (control bar visible): toggles play/pause

## Usage

### HTML

```html
<video id="player" src="<path_to_video_mp4>" playsinline></video>
```

### JavaScript

```js
import 'vlitejs/vlite.css';
import Vlitejs from 'vlitejs';
import VlitejsMobile from 'vlitejs/plugins/mobile/mobile.js';

Vlitejs.registerPlugin('mobile', VlitejsMobile);

new Vlitejs('#player', {
plugins: ['mobile']
});
```
83 changes: 83 additions & 0 deletions src/plugins/mobile/mobile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type Player from 'core/player.js'
import validateTarget from 'validate-target'

type pluginParameter = {
player: Player
options?: Record<string, unknown>
}

/**
* Vlitejs Mobile plugin
* @module Vlitejs/plugins/mobile
*/
export default class Mobile {
player: Player

providers = ['html5', 'youtube', 'dailymotion', 'vimeo']
types = ['video']

/**
* @constructor
* @param options
* @param options.player Player instance
*/
constructor({ player }: pluginParameter) {
this.player = player

this.onClickOnContainer = this.onClickOnContainer.bind(this)
}

/**
* Initialize
*/
init() {
this.player.isTouch && this.addEvents()
// TODO: add hotkeys in this plugin in the next major version
}

/**
* Add event listeners
*/
addEvents() {
this.player.elements.container.addEventListener('click', this.onClickOnContainer)
}

/**
* Handle click/tap on the container (only on touch devices)
* @param e Event data
*/
onClickOnContainer(e: Event) {
const target = e.target as HTMLElement

const validateTargetOverlay = validateTarget({
target,
selectorString: '.v-overlay',
nodeName: ['div']
})

if (validateTargetOverlay) {
this.onClickOnOverlay(e)
}
}

onClickOnOverlay(e: Event) {
const hasControlBar = !!this.player.elements.controlBar
const controlBarVisible = !this.player.elements.controlBar?.classList.contains('v-hidden')

if (hasControlBar && !controlBarVisible) {
this.player.Vlitejs.stopAutoHideTimer()
this.player.Vlitejs.autoHideGranted && this.player.Vlitejs.startAutoHideTimer()
return
}

// Control bar is visible, toggle the playback
this.player.controlBar.togglePlayPause(e)
}

/**
* Destroy the plugin
*/
destroy() {
this.player.elements.container.removeEventListener('click', this.onClickOnContainer)
}
}
49 changes: 49 additions & 0 deletions tests/html5.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,52 @@ test.describe('HTML5 Player Tests', () => {
expect(volume).toBe(0.1)
})
})

test.describe('HTML5 Player Mobile Overlay Tests', () => {
test.beforeEach(async ({ page }) => {
// Force a touch-like environment before any script runs
await page.addInitScript(() => {
Object.defineProperty(navigator, 'maxTouchPoints', {
get() {
return 1
},
configurable: true
})
})

await page.goto('http://localhost:3000/html5')

await page.waitForFunction(() => {
const video = document.querySelector('video')
return video && video.duration > 0
})
})

test('should show control bar on first overlay tap and pause on second', async ({ page }) => {
const video = page.locator('video')
const controlBar = page.locator('.v-controlBar')

await page.click('.v-bigPlay')

// Wait long enough for auto-hide to hide the control bar (autoHideDelay = 3000ms)
await page.waitForTimeout(3500)

const initiallyHidden = await controlBar.evaluate((el) => el.classList.contains('v-hidden'))

// First overlay tap: should show the control bar but keep playback running
await page.click('.v-overlay')
const controlBarVisibleAfterFirstTap = await controlBar.evaluate((el) =>
el.classList.contains('v-hidden')
)
const pausedAfterFirstTap = await video.evaluate((v) => v.paused)

// Second overlay tap: should pause the video
await page.click('.v-overlay')
const pausedAfterSecondTap = await video.evaluate((v) => v.paused)

expect(initiallyHidden).toBe(true)
expect(controlBarVisibleAfterFirstTap).toBe(false)
expect(pausedAfterFirstTap).toBe(false)
expect(pausedAfterSecondTap).toBe(true)
})
})
Loading