nanoplayer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +32 -0
- package/README.md +128 -0
- package/dist/nanoplayer.css +1 -0
- package/dist/nanoplayer.es.js +95 -0
- package/dist/nanoplayer.umd.js +7 -0
- package/package.json +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# NanoPlayer 🎬
|
|
2
|
+
|
|
3
|
+
**NanoPlayer** — это лёгкий, dependency-free HTML5 видеоплеер на чистом JavaScript
|
|
4
|
+
с интерфейсом в стиле YouTube.
|
|
5
|
+
|
|
6
|
+
Проект создан как **open-source** и может использоваться как в личных,
|
|
7
|
+
так и в коммерческих проектах.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Возможности
|
|
12
|
+
|
|
13
|
+
- ▶️ Play / Pause (кнопка, пробел)
|
|
14
|
+
- 🎞 Превью видео (poster или первый кадр автоматически)
|
|
15
|
+
- ⏱ Прогресс и перемотка
|
|
16
|
+
- 🔊 Громкость (выпадающее меню)
|
|
17
|
+
- ⚙️ Скорость воспроизведения
|
|
18
|
+
- ℹ️ Информация о плеере
|
|
19
|
+
- ⛶ Полноэкранный режим
|
|
20
|
+
- ⌨️ Управление с клавиатуры
|
|
21
|
+
- 🎨 Кастомный UI (без native controls)
|
|
22
|
+
- ❌ Без зависимостей
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📦 Установка
|
|
27
|
+
|
|
28
|
+
### Через npm (рекомендуется)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install nanoplayer
|
|
32
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# NanoPlayer 🎬
|
|
2
|
+
|
|
3
|
+
NanoPlayer — лёгкий, dependency-free HTML5 видеоплеер на чистом JavaScript
|
|
4
|
+
с интерфейсом в стиле YouTube.
|
|
5
|
+
|
|
6
|
+
Проект создаётся как open-source и подходит как для личных,
|
|
7
|
+
так и для коммерческих проектов.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Возможности
|
|
12
|
+
|
|
13
|
+
- Play / Pause (кнопка и пробел)
|
|
14
|
+
- Превью видео (poster или первый кадр автоматически)
|
|
15
|
+
- Прогресс и перемотка
|
|
16
|
+
- Громкость (выпадающее меню)
|
|
17
|
+
- Скорость воспроизведения
|
|
18
|
+
- Информация о плеере
|
|
19
|
+
- Полноэкранный режим
|
|
20
|
+
- Управление с клавиатуры
|
|
21
|
+
- Кастомный UI (без native controls)
|
|
22
|
+
- Без зависимостей
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Установка
|
|
27
|
+
|
|
28
|
+
Через npm:
|
|
29
|
+
```
|
|
30
|
+
npm install nanoplayer
|
|
31
|
+
```
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Быстрый старт
|
|
35
|
+
|
|
36
|
+
HTML (UMD):
|
|
37
|
+
```
|
|
38
|
+
<link rel="stylesheet" href="dist/style.css">
|
|
39
|
+
|
|
40
|
+
<div id="player"></div>
|
|
41
|
+
|
|
42
|
+
<script src="dist/nanoplayer.umd.js"></script>
|
|
43
|
+
<script>
|
|
44
|
+
new NanoPlayer('#player', {
|
|
45
|
+
src: 'video.mp4',
|
|
46
|
+
poster: 'poster.jpg'
|
|
47
|
+
})
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
ES Modules:
|
|
53
|
+
```
|
|
54
|
+
import NanoPlayer from 'nanoplayer'
|
|
55
|
+
import 'nanoplayer/dist/style.css'
|
|
56
|
+
|
|
57
|
+
new NanoPlayer('#player', {
|
|
58
|
+
src: 'video.mp4'
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
Важно: ES-модули должны запускаться через HTTP-сервер, не file://
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Параметры
|
|
66
|
+
|
|
67
|
+
new NanoPlayer(selector, options)
|
|
68
|
+
|
|
69
|
+
options:
|
|
70
|
+
|
|
71
|
+
- src (string) — путь к видео
|
|
72
|
+
- poster (string | null) — превью (если не указано, берётся первый кадр)
|
|
73
|
+
- autoplay (boolean) — автозапуск
|
|
74
|
+
- volume (number) — громкость (0–1)
|
|
75
|
+
- playbackRates (number[]) — доступные скорости
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Управление с клавиатуры
|
|
80
|
+
|
|
81
|
+
- Space — Play / Pause
|
|
82
|
+
- Esc — выход из fullscreen
|
|
83
|
+
|
|
84
|
+
Пробел не перехватывается, если фокус находится в input, textarea или select.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Стилизация
|
|
89
|
+
|
|
90
|
+
NanoPlayer не влияет на глобальные стили страницы.
|
|
91
|
+
Все CSS-классы имеют префикс nano-.
|
|
92
|
+
|
|
93
|
+
Можно:
|
|
94
|
+
- переопределять стили
|
|
95
|
+
- писать собственные темы
|
|
96
|
+
- полностью заменить style.css
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Архитектура
|
|
101
|
+
|
|
102
|
+
.nano-player
|
|
103
|
+
├── video
|
|
104
|
+
├── overlay (big play + info)
|
|
105
|
+
├── controls
|
|
106
|
+
│ ├── left (play)
|
|
107
|
+
│ ├── center (progress)
|
|
108
|
+
│ └── right (menus)
|
|
109
|
+
│ ├── volume
|
|
110
|
+
│ ├── settings
|
|
111
|
+
│ └── info
|
|
112
|
+
|
|
113
|
+
Каждое меню — отдельный popover, как в YouTube.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Структура проекта
|
|
118
|
+
|
|
119
|
+
nanoplayer/
|
|
120
|
+
├── src/
|
|
121
|
+
│ ├── NanoPlayer.js
|
|
122
|
+
│ └── index.js
|
|
123
|
+
│ └── style.css
|
|
124
|
+
├── dist/
|
|
125
|
+
│ ├── nanoplayer.umd.js
|
|
126
|
+
│ └── nanoplayer.es.js
|
|
127
|
+
│ └── nanoplayer.umd.js
|
|
128
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.nano-player{position:relative;max-width:720px;aspect-ratio:16 / 9;background:#000;border-radius:12px;overflow:hidden;font-family:system-ui,sans-serif}.nano-player video{width:100%;height:100%;display:block}.nano-overlay{position:absolute;inset:0;background:#0006;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px}.nano-overlay.hidden{display:none}.nano-big-play{width:80px;height:80px;border-radius:50%;border:none;font-size:32px;cursor:pointer}.nano-info-overlay{color:#fff;font-size:14px;text-align:center}.nano-controls{position:absolute;bottom:0;left:0;right:0;display:grid;grid-template-columns:auto 1fr auto;gap:10px;padding:8px;background:linear-gradient(to top,rgba(0,0,0,.7),transparent)}.nano-left,.nano-center,.nano-right{display:flex;align-items:center;gap:6px}.nano-icon{background:none;border:none;color:#fff;font-size:16px;cursor:pointer}.nano-progress{width:100%;height:6px;background:#ffffff4d;cursor:pointer}.nano-progress-fill{height:100%;background:#fff;width:0%}.nano-menu-wrap{position:relative}.nano-menu{position:absolute;bottom:36px;right:0;min-width:140px;background:#000000e6;border-radius:6px;padding:8px;display:none;color:#fff;font-size:13px}.nano-menu-wrap.open .nano-menu{display:block}.nano-menu button{width:100%;background:none;border:none;color:#fff;padding:6px;cursor:pointer;text-align:left}.nano-menu button:hover{background:#ffffff1a}.nano-menu input[type=range]{width:100%}.nano-player.nano-fullscreen{width:100vw;height:100vh;max-width:none;border-radius:0}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
class l {
|
|
2
|
+
constructor(e, t = {}) {
|
|
3
|
+
if (this.container = document.querySelector(e), !this.container)
|
|
4
|
+
throw new Error("NanoPlayer: container not found");
|
|
5
|
+
this.options = {
|
|
6
|
+
src: "",
|
|
7
|
+
poster: null,
|
|
8
|
+
autoplay: !1,
|
|
9
|
+
volume: 1,
|
|
10
|
+
playbackRates: [0.5, 1, 1.5, 2],
|
|
11
|
+
...t
|
|
12
|
+
}, this.init();
|
|
13
|
+
}
|
|
14
|
+
/* ================= INIT ================= */
|
|
15
|
+
init() {
|
|
16
|
+
this.wrapper = document.createElement("div"), this.wrapper.className = "nano-player", this.wrapper.tabIndex = 0, this.video = document.createElement("video"), this.video.src = this.options.src, this.video.autoplay = this.options.autoplay, this.video.volume = this.options.volume, this.video.preload = "metadata", this.video.controls = !1, this.options.poster && (this.video.poster = this.options.poster), this.wrapper.appendChild(this.video), this.container.appendChild(this.wrapper), this.createOverlay(), this.createControls(), this.bindEvents();
|
|
17
|
+
}
|
|
18
|
+
/* ================= OVERLAY ================= */
|
|
19
|
+
createOverlay() {
|
|
20
|
+
this.overlay = document.createElement("div"), this.overlay.className = "nano-overlay", this.bigPlay = document.createElement("button"), this.bigPlay.className = "nano-big-play", this.bigPlay.textContent = "▶", this.overlay.append(this.bigPlay, this.infoOverlay), this.wrapper.appendChild(this.overlay), this.bigPlay.onclick = () => this.video.play();
|
|
21
|
+
}
|
|
22
|
+
/* ================= CONTROLS ================= */
|
|
23
|
+
createControls() {
|
|
24
|
+
this.controls = document.createElement("div"), this.controls.className = "nano-controls", this.left = document.createElement("div"), this.left.className = "nano-left", this.playBtn = document.createElement("button"), this.playBtn.className = "nano-icon", this.playBtn.textContent = "▶", this.left.appendChild(this.playBtn), this.center = document.createElement("div"), this.center.className = "nano-center", this.progress = document.createElement("div"), this.progress.className = "nano-progress", this.progressFill = document.createElement("div"), this.progressFill.className = "nano-progress-fill", this.progress.appendChild(this.progressFill), this.center.appendChild(this.progress), this.right = document.createElement("div"), this.right.className = "nano-right", this.volumeMenu = this.createMenu("🔊"), this.volumeSlider = document.createElement("input"), this.volumeSlider.type = "range", this.volumeSlider.min = 0, this.volumeSlider.max = 1, this.volumeSlider.step = 0.01, this.volumeSlider.value = this.video.volume, this.volumeMenu.menu.appendChild(this.volumeSlider), this.settingsMenu = this.createMenu("⚙️"), this.options.playbackRates.forEach((e) => {
|
|
25
|
+
const t = document.createElement("button");
|
|
26
|
+
t.textContent = `${e}x`, t.onclick = () => {
|
|
27
|
+
this.video.playbackRate = e, this.settingsMenu.close();
|
|
28
|
+
}, this.settingsMenu.menu.appendChild(t);
|
|
29
|
+
}), this.infoMenu = this.createMenu("ℹ️"), this.infoMenu.menu.innerHTML = `
|
|
30
|
+
<strong>NanoPlayer</strong><br>
|
|
31
|
+
Version 0.1.0<br>
|
|
32
|
+
MIT License
|
|
33
|
+
swiftmessage.org
|
|
34
|
+
github.com/swiftmessage/NanoPlayer
|
|
35
|
+
`, this.fullscreenBtn = document.createElement("button"), this.fullscreenBtn.className = "nano-icon", this.fullscreenBtn.textContent = "⛶", this.fullscreenBtn.onclick = () => this.toggleFullscreen(), this.right.append(
|
|
36
|
+
this.volumeMenu.root,
|
|
37
|
+
this.settingsMenu.root,
|
|
38
|
+
this.infoMenu.root,
|
|
39
|
+
this.fullscreenBtn
|
|
40
|
+
), this.controls.append(this.left, this.center, this.right), this.wrapper.appendChild(this.controls), this.playBtn.onclick = () => this.video.paused ? this.video.play() : this.video.pause(), this.progress.onclick = (e) => {
|
|
41
|
+
const t = this.progress.getBoundingClientRect();
|
|
42
|
+
this.video.currentTime = (e.clientX - t.left) / t.width * this.video.duration;
|
|
43
|
+
}, this.volumeSlider.oninput = () => this.video.volume = this.volumeSlider.value;
|
|
44
|
+
}
|
|
45
|
+
/* ================= MENU HELPER ================= */
|
|
46
|
+
createMenu(e) {
|
|
47
|
+
const t = document.createElement("div");
|
|
48
|
+
t.className = "nano-menu-wrap";
|
|
49
|
+
const s = document.createElement("button");
|
|
50
|
+
s.className = "nano-icon", s.textContent = e;
|
|
51
|
+
const i = document.createElement("div");
|
|
52
|
+
return i.className = "nano-menu", s.onclick = (n) => {
|
|
53
|
+
n.stopPropagation(), document.querySelectorAll(".nano-menu-wrap.open").forEach((o) => o.classList.remove("open")), t.classList.toggle("open");
|
|
54
|
+
}, document.addEventListener("click", () => {
|
|
55
|
+
t.classList.remove("open");
|
|
56
|
+
}), t.append(s, i), {
|
|
57
|
+
root: t,
|
|
58
|
+
menu: i,
|
|
59
|
+
close: () => t.classList.remove("open")
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/* ================= EVENTS ================= */
|
|
63
|
+
bindEvents() {
|
|
64
|
+
this.wrapper.addEventListener("click", () => {
|
|
65
|
+
this.wrapper.focus();
|
|
66
|
+
}), this.video.addEventListener("loadedmetadata", () => {
|
|
67
|
+
this.options.poster || (this.video.currentTime = 0.1, this.video.pause());
|
|
68
|
+
}), this.video.addEventListener("play", () => {
|
|
69
|
+
this.playBtn.textContent = "❚❚", this.overlay.classList.add("hidden");
|
|
70
|
+
}), this.video.addEventListener("pause", () => {
|
|
71
|
+
this.playBtn.textContent = "▶";
|
|
72
|
+
}), this.video.addEventListener("timeupdate", () => {
|
|
73
|
+
const e = this.video.currentTime / this.video.duration * 100;
|
|
74
|
+
this.progressFill.style.width = `${e}%`;
|
|
75
|
+
}), this.wrapper.addEventListener("keydown", (e) => {
|
|
76
|
+
if (e.code !== "Space") return;
|
|
77
|
+
const t = e.target.tagName;
|
|
78
|
+
["INPUT", "TEXTAREA", "SELECT"].includes(t) || (e.preventDefault(), this.video.paused ? this.video.play() : this.video.pause());
|
|
79
|
+
}), document.addEventListener("fullscreenchange", () => {
|
|
80
|
+
this.wrapper.classList.toggle(
|
|
81
|
+
"nano-fullscreen",
|
|
82
|
+
!!document.fullscreenElement
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/* ================= FULLSCREEN ================= */
|
|
87
|
+
toggleFullscreen() {
|
|
88
|
+
const e = this.wrapper;
|
|
89
|
+
document.fullscreenElement ? document.exitFullscreen ? document.exitFullscreen() : document.webkitExitFullscreen ? document.webkitExitFullscreen() : document.msExitFullscreen && document.msExitFullscreen() : e.requestFullscreen ? e.requestFullscreen() : e.webkitRequestFullscreen ? e.webkitRequestFullscreen() : e.msRequestFullscreen && e.msRequestFullscreen();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
typeof window < "u" && (window.NanoPlayer = l);
|
|
93
|
+
export {
|
|
94
|
+
l as default
|
|
95
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
(function(s,i){typeof exports=="object"&&typeof module<"u"?module.exports=i():typeof define=="function"&&define.amd?define(i):(s=typeof globalThis<"u"?globalThis:s||self,s.NanoPlayer=i())})(this,(function(){"use strict";class s{constructor(e,t={}){if(this.container=document.querySelector(e),!this.container)throw new Error("NanoPlayer: container not found");this.options={src:"",poster:null,autoplay:!1,volume:1,playbackRates:[.5,1,1.5,2],...t},this.init()}init(){this.wrapper=document.createElement("div"),this.wrapper.className="nano-player",this.wrapper.tabIndex=0,this.video=document.createElement("video"),this.video.src=this.options.src,this.video.autoplay=this.options.autoplay,this.video.volume=this.options.volume,this.video.preload="metadata",this.video.controls=!1,this.options.poster&&(this.video.poster=this.options.poster),this.wrapper.appendChild(this.video),this.container.appendChild(this.wrapper),this.createOverlay(),this.createControls(),this.bindEvents()}createOverlay(){this.overlay=document.createElement("div"),this.overlay.className="nano-overlay",this.bigPlay=document.createElement("button"),this.bigPlay.className="nano-big-play",this.bigPlay.textContent="▶",this.overlay.append(this.bigPlay,this.infoOverlay),this.wrapper.appendChild(this.overlay),this.bigPlay.onclick=()=>this.video.play()}createControls(){this.controls=document.createElement("div"),this.controls.className="nano-controls",this.left=document.createElement("div"),this.left.className="nano-left",this.playBtn=document.createElement("button"),this.playBtn.className="nano-icon",this.playBtn.textContent="▶",this.left.appendChild(this.playBtn),this.center=document.createElement("div"),this.center.className="nano-center",this.progress=document.createElement("div"),this.progress.className="nano-progress",this.progressFill=document.createElement("div"),this.progressFill.className="nano-progress-fill",this.progress.appendChild(this.progressFill),this.center.appendChild(this.progress),this.right=document.createElement("div"),this.right.className="nano-right",this.volumeMenu=this.createMenu("🔊"),this.volumeSlider=document.createElement("input"),this.volumeSlider.type="range",this.volumeSlider.min=0,this.volumeSlider.max=1,this.volumeSlider.step=.01,this.volumeSlider.value=this.video.volume,this.volumeMenu.menu.appendChild(this.volumeSlider),this.settingsMenu=this.createMenu("⚙️"),this.options.playbackRates.forEach(e=>{const t=document.createElement("button");t.textContent=`${e}x`,t.onclick=()=>{this.video.playbackRate=e,this.settingsMenu.close()},this.settingsMenu.menu.appendChild(t)}),this.infoMenu=this.createMenu("ℹ️"),this.infoMenu.menu.innerHTML=`
|
|
2
|
+
<strong>NanoPlayer</strong><br>
|
|
3
|
+
Version 0.1.0<br>
|
|
4
|
+
MIT License
|
|
5
|
+
swiftmessage.org
|
|
6
|
+
github.com/swiftmessage/NanoPlayer
|
|
7
|
+
`,this.fullscreenBtn=document.createElement("button"),this.fullscreenBtn.className="nano-icon",this.fullscreenBtn.textContent="⛶",this.fullscreenBtn.onclick=()=>this.toggleFullscreen(),this.right.append(this.volumeMenu.root,this.settingsMenu.root,this.infoMenu.root,this.fullscreenBtn),this.controls.append(this.left,this.center,this.right),this.wrapper.appendChild(this.controls),this.playBtn.onclick=()=>this.video.paused?this.video.play():this.video.pause(),this.progress.onclick=e=>{const t=this.progress.getBoundingClientRect();this.video.currentTime=(e.clientX-t.left)/t.width*this.video.duration},this.volumeSlider.oninput=()=>this.video.volume=this.volumeSlider.value}createMenu(e){const t=document.createElement("div");t.className="nano-menu-wrap";const n=document.createElement("button");n.className="nano-icon",n.textContent=e;const o=document.createElement("div");return o.className="nano-menu",n.onclick=l=>{l.stopPropagation(),document.querySelectorAll(".nano-menu-wrap.open").forEach(r=>r.classList.remove("open")),t.classList.toggle("open")},document.addEventListener("click",()=>{t.classList.remove("open")}),t.append(n,o),{root:t,menu:o,close:()=>t.classList.remove("open")}}bindEvents(){this.wrapper.addEventListener("click",()=>{this.wrapper.focus()}),this.video.addEventListener("loadedmetadata",()=>{this.options.poster||(this.video.currentTime=.1,this.video.pause())}),this.video.addEventListener("play",()=>{this.playBtn.textContent="❚❚",this.overlay.classList.add("hidden")}),this.video.addEventListener("pause",()=>{this.playBtn.textContent="▶"}),this.video.addEventListener("timeupdate",()=>{const e=this.video.currentTime/this.video.duration*100;this.progressFill.style.width=`${e}%`}),this.wrapper.addEventListener("keydown",e=>{if(e.code!=="Space")return;const t=e.target.tagName;["INPUT","TEXTAREA","SELECT"].includes(t)||(e.preventDefault(),this.video.paused?this.video.play():this.video.pause())}),document.addEventListener("fullscreenchange",()=>{this.wrapper.classList.toggle("nano-fullscreen",!!document.fullscreenElement)})}toggleFullscreen(){const e=this.wrapper;document.fullscreenElement?document.exitFullscreen?document.exitFullscreen():document.webkitExitFullscreen?document.webkitExitFullscreen():document.msExitFullscreen&&document.msExitFullscreen():e.requestFullscreen?e.requestFullscreen():e.webkitRequestFullscreen?e.webkitRequestFullscreen():e.msRequestFullscreen&&e.msRequestFullscreen()}}return typeof window<"u"&&(window.NanoPlayer=s),s}));
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nanoplayer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NanoPlayer — lightweight vanilla JavaScript video player",
|
|
5
|
+
"main": "dist/nanoplayer.umd.js",
|
|
6
|
+
"module": "dist/nanoplayer.es.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "vite build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"video",
|
|
15
|
+
"player",
|
|
16
|
+
"html5",
|
|
17
|
+
"vanilla-js",
|
|
18
|
+
"nano"
|
|
19
|
+
],
|
|
20
|
+
"author": "SWIFTMESSAGE",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"vite": "^7.3.1"
|
|
24
|
+
}
|
|
25
|
+
}
|