@jjlmoya/utils-audiovisual 1.2.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/package.json +60 -0
- package/src/category/i18n/en.ts +198 -0
- package/src/category/i18n/es.ts +198 -0
- package/src/category/i18n/fr.ts +198 -0
- package/src/category/index.ts +17 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +4 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +32 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/chromaticLens/bibliography.astro +17 -0
- package/src/tool/chromaticLens/component.astro +178 -0
- package/src/tool/chromaticLens/i18n/en.ts +246 -0
- package/src/tool/chromaticLens/i18n/es.ts +244 -0
- package/src/tool/chromaticLens/i18n/fr.ts +244 -0
- package/src/tool/chromaticLens/index.ts +43 -0
- package/src/tool/chromaticLens/logic.ts +87 -0
- package/src/tool/chromaticLens/seo.astro +15 -0
- package/src/tool/chromaticLens/style.css +308 -0
- package/src/tool/chromaticLens/ui.ts +109 -0
- package/src/tool/collageMaker/bibliography.astro +17 -0
- package/src/tool/collageMaker/component.astro +302 -0
- package/src/tool/collageMaker/i18n/en.ts +233 -0
- package/src/tool/collageMaker/i18n/es.ts +231 -0
- package/src/tool/collageMaker/i18n/fr.ts +231 -0
- package/src/tool/collageMaker/index.ts +51 -0
- package/src/tool/collageMaker/logic.ts +134 -0
- package/src/tool/collageMaker/seo.astro +15 -0
- package/src/tool/collageMaker/style.css +386 -0
- package/src/tool/exifCleaner/bibliography.astro +18 -0
- package/src/tool/exifCleaner/component.astro +162 -0
- package/src/tool/exifCleaner/i18n/en.ts +277 -0
- package/src/tool/exifCleaner/i18n/es.ts +277 -0
- package/src/tool/exifCleaner/i18n/fr.ts +277 -0
- package/src/tool/exifCleaner/index.ts +57 -0
- package/src/tool/exifCleaner/logic.ts +135 -0
- package/src/tool/exifCleaner/seo.astro +18 -0
- package/src/tool/exifCleaner/style.css +289 -0
- package/src/tool/exifCleaner/ui.ts +117 -0
- package/src/tool/imageCompressor/bibliography.astro +17 -0
- package/src/tool/imageCompressor/component.astro +262 -0
- package/src/tool/imageCompressor/i18n/en.ts +232 -0
- package/src/tool/imageCompressor/i18n/es.ts +230 -0
- package/src/tool/imageCompressor/i18n/fr.ts +230 -0
- package/src/tool/imageCompressor/index.ts +50 -0
- package/src/tool/imageCompressor/logic.ts +79 -0
- package/src/tool/imageCompressor/seo.astro +15 -0
- package/src/tool/imageCompressor/style.css +503 -0
- package/src/tool/printQualityCalculator/bibliography.astro +18 -0
- package/src/tool/printQualityCalculator/component.astro +318 -0
- package/src/tool/printQualityCalculator/i18n/en.ts +247 -0
- package/src/tool/printQualityCalculator/i18n/es.ts +245 -0
- package/src/tool/printQualityCalculator/i18n/fr.ts +245 -0
- package/src/tool/printQualityCalculator/index.ts +56 -0
- package/src/tool/printQualityCalculator/logic.ts +53 -0
- package/src/tool/printQualityCalculator/seo.astro +18 -0
- package/src/tool/printQualityCalculator/style.css +491 -0
- package/src/tool/printQualityCalculator/ui.ts +122 -0
- package/src/tool/privacyBlur/bibliography.astro +17 -0
- package/src/tool/privacyBlur/component.astro +230 -0
- package/src/tool/privacyBlur/i18n/en.ts +238 -0
- package/src/tool/privacyBlur/i18n/es.ts +236 -0
- package/src/tool/privacyBlur/i18n/fr.ts +236 -0
- package/src/tool/privacyBlur/index.ts +49 -0
- package/src/tool/privacyBlur/logic.ts +249 -0
- package/src/tool/privacyBlur/seo.astro +15 -0
- package/src/tool/privacyBlur/style.css +332 -0
- package/src/tool/privacyBlur/ui.ts +124 -0
- package/src/tool/subtitleSync/bibliography.astro +17 -0
- package/src/tool/subtitleSync/component.astro +187 -0
- package/src/tool/subtitleSync/i18n/en.ts +241 -0
- package/src/tool/subtitleSync/i18n/es.ts +241 -0
- package/src/tool/subtitleSync/i18n/fr.ts +241 -0
- package/src/tool/subtitleSync/index.ts +49 -0
- package/src/tool/subtitleSync/logic.ts +91 -0
- package/src/tool/subtitleSync/seo.astro +15 -0
- package/src/tool/subtitleSync/style.css +325 -0
- package/src/tool/subtitleSync/ui.ts +152 -0
- package/src/tool/timelapseCalculator/bibliography.astro +15 -0
- package/src/tool/timelapseCalculator/component.astro +148 -0
- package/src/tool/timelapseCalculator/i18n/en.ts +169 -0
- package/src/tool/timelapseCalculator/i18n/es.ts +169 -0
- package/src/tool/timelapseCalculator/i18n/fr.ts +169 -0
- package/src/tool/timelapseCalculator/index.ts +52 -0
- package/src/tool/timelapseCalculator/logic.ts +46 -0
- package/src/tool/timelapseCalculator/seo.astro +18 -0
- package/src/tool/timelapseCalculator/style.css +285 -0
- package/src/tool/tvDistance/bibliography.astro +17 -0
- package/src/tool/tvDistance/component.astro +178 -0
- package/src/tool/tvDistance/i18n/en.ts +223 -0
- package/src/tool/tvDistance/i18n/es.ts +223 -0
- package/src/tool/tvDistance/i18n/fr.ts +223 -0
- package/src/tool/tvDistance/index.ts +49 -0
- package/src/tool/tvDistance/logic.ts +47 -0
- package/src/tool/tvDistance/seo.astro +15 -0
- package/src/tool/tvDistance/style.css +435 -0
- package/src/tool/tvDistance/ui.ts +66 -0
- package/src/tool/videoFrameExtractor/bibliography.astro +17 -0
- package/src/tool/videoFrameExtractor/component.astro +285 -0
- package/src/tool/videoFrameExtractor/i18n/en.ts +235 -0
- package/src/tool/videoFrameExtractor/i18n/es.ts +235 -0
- package/src/tool/videoFrameExtractor/i18n/fr.ts +235 -0
- package/src/tool/videoFrameExtractor/index.ts +53 -0
- package/src/tool/videoFrameExtractor/logic.ts +49 -0
- package/src/tool/videoFrameExtractor/seo.astro +15 -0
- package/src/tool/videoFrameExtractor/style.css +426 -0
- package/src/tool/videoFrameExtractor/ui.ts +179 -0
- package/src/tools.ts +25 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { VideoFrameExtractorUI } from './index';
|
|
3
|
+
import './style.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: VideoFrameExtractorUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="vfe-root" id="vfe-root" data-ui={JSON.stringify(ui)}>
|
|
13
|
+
<div class="vfe-premium-card">
|
|
14
|
+
|
|
15
|
+
<div id="vfe-upload-box" class="vfe-uploader-box">
|
|
16
|
+
<input type="file" id="vfe-file-input" accept="video/*" class="vfe-hidden" />
|
|
17
|
+
<div class="vfe-uploader-icon">
|
|
18
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
19
|
+
<path d="M17 10.5V7a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11zM14 13h-3v3H9v-3H6v-2h3V8h2v3h3z"/>
|
|
20
|
+
</svg>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="vfe-uploader-text">
|
|
23
|
+
<h3>{ui.uploadTitle}</h3>
|
|
24
|
+
<p>{ui.uploadFormats}</p>
|
|
25
|
+
</div>
|
|
26
|
+
<span class="vfe-privacy-note">{ui.privacyNote}</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div id="vfe-player-area" class="vfe-player-container vfe-hidden">
|
|
30
|
+
<div class="vfe-video-wrapper">
|
|
31
|
+
<video id="vfe-video" preload="metadata"></video>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="vfe-controls-glass">
|
|
35
|
+
<div class="vfe-time-row">
|
|
36
|
+
<span id="vfe-current-time">00:00.000</span>
|
|
37
|
+
<span id="vfe-duration">00:00.000</span>
|
|
38
|
+
</div>
|
|
39
|
+
<input type="range" id="vfe-scrubber" class="vfe-scrubber" min="0" step="0.001" value="0" />
|
|
40
|
+
|
|
41
|
+
<div class="vfe-actions-row">
|
|
42
|
+
<button id="vfe-prev" class="vfe-btn-main vfe-btn-control" title={ui.prevFrame}>
|
|
43
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M15.41 16.58L10.83 12l4.58-4.59L14 6l-6 6 6 6z"/></svg>
|
|
44
|
+
<span>-1F</span>
|
|
45
|
+
</button>
|
|
46
|
+
<button id="vfe-play" class="vfe-btn-main vfe-btn-control">
|
|
47
|
+
<svg id="vfe-play-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.14v14l11-7z"/></svg>
|
|
48
|
+
<svg id="vfe-pause-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="vfe-hidden"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
|
49
|
+
<span id="vfe-play-text">{ui.playLabel}</span>
|
|
50
|
+
</button>
|
|
51
|
+
<button id="vfe-next" class="vfe-btn-main vfe-btn-control" title={ui.nextFrame}>
|
|
52
|
+
<span>+1F</span>
|
|
53
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="vfe-actions-row">
|
|
58
|
+
<button id="vfe-capture" class="vfe-btn-main vfe-btn-capture">
|
|
59
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20 5h-3.17L15 3H9L7.17 5H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-8 13c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
|
60
|
+
<span>{ui.captureBtn}</span>
|
|
61
|
+
</button>
|
|
62
|
+
<button id="vfe-reset" class="vfe-btn-main vfe-btn-control vfe-btn-icon-only" title={ui.resetBtn}>
|
|
63
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 0 0-8 8 8 8 0 0 0 8 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18a6 6 0 0 1-6-6 6 6 0 0 1 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4z"/></svg>
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="vfe-batch-panel">
|
|
68
|
+
<div class="vfe-batch-header">
|
|
69
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.6L5 7l1.4-2.5L5 2l2.5 1.4L10 2L8.6 4.5L10 7zm12 9.8L22 14l-1.4 2.5L22 19l-2.5-1.4L17 19l1.4-2.5L17 14zM22 2l-1.4 2.5L22 7l-2.5-1.4L17 7l1.4-2.5L17 2l2.5 1.4zm-8.66 10.78l2.44-2.44-2.12-2.12-2.44 2.44zm1.03-5.49l2.34 2.34c.39.37.39 1.02 0 1.41L5.04 22.71c-.39.39-1.04.39-1.41 0l-2.34-2.34c-.39-.37-.39-1.02 0-1.41L12.96 7.29c.39-.39 1.04-.39 1.41 0"/></svg>
|
|
70
|
+
<span>{ui.batchTitle}</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="vfe-batch-controls">
|
|
73
|
+
<div class="vfe-batch-input-group">
|
|
74
|
+
<label>{ui.batchEvery}</label>
|
|
75
|
+
<input type="number" id="vfe-batch-interval" value="1" min="0.1" step="0.1" />
|
|
76
|
+
<label>s</label>
|
|
77
|
+
</div>
|
|
78
|
+
<button id="vfe-batch-start" class="vfe-btn-main vfe-btn-capture vfe-btn-batch">
|
|
79
|
+
<span id="vfe-batch-label">{ui.batchStart}</span>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="vfe-gallery-minimal">
|
|
86
|
+
<div class="vfe-gallery-header">
|
|
87
|
+
<h4>{ui.galleryTitle}</h4>
|
|
88
|
+
<button id="vfe-download-all" class="vfe-btn-main vfe-btn-control vfe-btn-sm vfe-hidden">
|
|
89
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 1v6H5l7 7 7-7h-4V1zM5 16v2h14v-2zm0 4v2h14v-2z"/></svg>
|
|
90
|
+
<span>{ui.downloadAll}</span>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
<div id="vfe-frames-gallery" class="vfe-frames-scroll">
|
|
94
|
+
<p id="vfe-gallery-empty" class="vfe-gallery-empty-text">{ui.galleryEmpty}</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div id="vfe-lightbox" class="vfe-lightbox">
|
|
103
|
+
<div class="vfe-lightbox-content">
|
|
104
|
+
<span id="vfe-lightbox-close" class="vfe-lightbox-close">×</span>
|
|
105
|
+
<img id="vfe-lightbox-img" src="" alt="" class="vfe-lightbox-img" />
|
|
106
|
+
<div class="vfe-actions-row" style="width:100%;justify-content:center">
|
|
107
|
+
<a id="vfe-lightbox-down" href="" download="frame.webp" class="vfe-btn-main vfe-btn-capture">
|
|
108
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z"/></svg>
|
|
109
|
+
<span>{ui.downloadHD}</span>
|
|
110
|
+
</a>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<script>
|
|
116
|
+
import { captureFrameFromVideo, captureFrameAtTime, formatTime } from './logic';
|
|
117
|
+
import type { CapturedFrame } from './logic';
|
|
118
|
+
import type { VideoFrameExtractorUI } from './index';
|
|
119
|
+
|
|
120
|
+
function init() {
|
|
121
|
+
const root = document.getElementById('vfe-root');
|
|
122
|
+
if (!root) return;
|
|
123
|
+
|
|
124
|
+
const labels = JSON.parse(root.dataset.ui ?? '{}') as VideoFrameExtractorUI;
|
|
125
|
+
|
|
126
|
+
const fileInput = root.querySelector('#vfe-file-input') as HTMLInputElement;
|
|
127
|
+
const uploadBox = root.querySelector('#vfe-upload-box') as HTMLElement;
|
|
128
|
+
const playerArea = root.querySelector('#vfe-player-area') as HTMLElement;
|
|
129
|
+
const video = root.querySelector('#vfe-video') as HTMLVideoElement;
|
|
130
|
+
const playBtn = root.querySelector('#vfe-play') as HTMLButtonElement;
|
|
131
|
+
const playIcon = root.querySelector('#vfe-play-icon') as HTMLElement;
|
|
132
|
+
const pauseIcon = root.querySelector('#vfe-pause-icon') as HTMLElement;
|
|
133
|
+
const playText = root.querySelector('#vfe-play-text') as HTMLElement;
|
|
134
|
+
const prevBtn = root.querySelector('#vfe-prev') as HTMLButtonElement;
|
|
135
|
+
const nextBtn = root.querySelector('#vfe-next') as HTMLButtonElement;
|
|
136
|
+
const captureBtn = root.querySelector('#vfe-capture') as HTMLButtonElement;
|
|
137
|
+
const resetBtn = root.querySelector('#vfe-reset') as HTMLButtonElement;
|
|
138
|
+
const scrubber = root.querySelector('#vfe-scrubber') as HTMLInputElement;
|
|
139
|
+
const currentTimeEl = root.querySelector('#vfe-current-time') as HTMLElement;
|
|
140
|
+
const durationEl = root.querySelector('#vfe-duration') as HTMLElement;
|
|
141
|
+
const batchInterval = root.querySelector('#vfe-batch-interval') as HTMLInputElement;
|
|
142
|
+
const batchStartBtn = root.querySelector('#vfe-batch-start') as HTMLButtonElement;
|
|
143
|
+
const batchLabelEl = root.querySelector('#vfe-batch-label') as HTMLElement;
|
|
144
|
+
const framesGallery = root.querySelector('#vfe-frames-gallery') as HTMLElement;
|
|
145
|
+
const galleryEmpty = root.querySelector('#vfe-gallery-empty') as HTMLElement;
|
|
146
|
+
const downloadAllBtn = root.querySelector('#vfe-download-all') as HTMLElement;
|
|
147
|
+
const lightbox = document.getElementById('vfe-lightbox') as HTMLElement;
|
|
148
|
+
const lightboxImg = document.getElementById('vfe-lightbox-img') as HTMLImageElement;
|
|
149
|
+
const lightboxDown = document.getElementById('vfe-lightbox-down') as HTMLAnchorElement;
|
|
150
|
+
|
|
151
|
+
let captured: CapturedFrame[] = [];
|
|
152
|
+
const FRAME_STEP = 1 / 30;
|
|
153
|
+
|
|
154
|
+
function handleFile(file: File) {
|
|
155
|
+
video.src = URL.createObjectURL(file);
|
|
156
|
+
uploadBox.classList.add('vfe-hidden');
|
|
157
|
+
playerArea.classList.remove('vfe-hidden');
|
|
158
|
+
video.load();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function addFrame(frame: CapturedFrame) {
|
|
162
|
+
captured.push(frame);
|
|
163
|
+
galleryEmpty.classList.add('vfe-hidden');
|
|
164
|
+
downloadAllBtn.classList.remove('vfe-hidden');
|
|
165
|
+
|
|
166
|
+
const card = document.createElement('div');
|
|
167
|
+
card.className = 'vfe-frame-card';
|
|
168
|
+
card.innerHTML = `
|
|
169
|
+
<img src="${frame.url}" class="vfe-frame-thumb" alt="" />
|
|
170
|
+
<div class="vfe-frame-footer">
|
|
171
|
+
<span class="vfe-frame-time">${formatTime(frame.timestamp)}</span>
|
|
172
|
+
<a href="${frame.url}" download="frame_${frame.timestamp.toFixed(3)}.webp" class="vfe-btn-main vfe-btn-control vfe-btn-sm">
|
|
173
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z"/></svg>
|
|
174
|
+
</a>
|
|
175
|
+
</div>
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
(card.querySelector('.vfe-frame-thumb') as HTMLElement).addEventListener('click', () => {
|
|
179
|
+
lightboxImg.src = frame.url;
|
|
180
|
+
lightboxDown.href = frame.url;
|
|
181
|
+
lightbox.classList.add('vfe-lightbox-open');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
framesGallery.prepend(card);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
uploadBox.addEventListener('click', () => fileInput.click());
|
|
188
|
+
fileInput.addEventListener('change', () => {
|
|
189
|
+
if (fileInput.files?.[0]) handleFile(fileInput.files[0]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
root.addEventListener('dragover', (e) => { e.preventDefault(); uploadBox.classList.add('vfe-dragover'); });
|
|
193
|
+
root.addEventListener('dragleave', () => uploadBox.classList.remove('vfe-dragover'));
|
|
194
|
+
root.addEventListener('drop', (e) => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
uploadBox.classList.remove('vfe-dragover');
|
|
197
|
+
if (e.dataTransfer?.files[0]) handleFile(e.dataTransfer.files[0]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
video.addEventListener('loadedmetadata', () => {
|
|
201
|
+
durationEl.textContent = formatTime(video.duration);
|
|
202
|
+
scrubber.max = video.duration.toString();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
video.addEventListener('timeupdate', () => {
|
|
206
|
+
currentTimeEl.textContent = formatTime(video.currentTime);
|
|
207
|
+
scrubber.value = video.currentTime.toString();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
video.addEventListener('ended', () => {
|
|
211
|
+
playIcon.classList.remove('vfe-hidden');
|
|
212
|
+
pauseIcon.classList.add('vfe-hidden');
|
|
213
|
+
playText.textContent = labels.playLabel;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
scrubber.addEventListener('input', () => { video.currentTime = parseFloat(scrubber.value); });
|
|
217
|
+
|
|
218
|
+
playBtn.addEventListener('click', () => {
|
|
219
|
+
if (video.paused) {
|
|
220
|
+
video.play();
|
|
221
|
+
playIcon.classList.add('vfe-hidden');
|
|
222
|
+
pauseIcon.classList.remove('vfe-hidden');
|
|
223
|
+
playText.textContent = labels.pauseLabel;
|
|
224
|
+
} else {
|
|
225
|
+
video.pause();
|
|
226
|
+
playIcon.classList.remove('vfe-hidden');
|
|
227
|
+
pauseIcon.classList.add('vfe-hidden');
|
|
228
|
+
playText.textContent = labels.playLabel;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
prevBtn.addEventListener('click', () => { video.pause(); video.currentTime = Math.max(0, video.currentTime - FRAME_STEP); });
|
|
233
|
+
nextBtn.addEventListener('click', () => { video.pause(); video.currentTime = Math.min(video.duration, video.currentTime + FRAME_STEP); });
|
|
234
|
+
|
|
235
|
+
captureBtn.addEventListener('click', () => {
|
|
236
|
+
const frame = captureFrameFromVideo(video);
|
|
237
|
+
if (frame) addFrame(frame);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
batchStartBtn.addEventListener('click', async () => {
|
|
241
|
+
const interval = parseFloat(batchInterval.value);
|
|
242
|
+
if (isNaN(interval) || interval <= 0) return;
|
|
243
|
+
batchStartBtn.disabled = true;
|
|
244
|
+
batchLabelEl.textContent = labels.batchProcessing;
|
|
245
|
+
for (let t = 0; t <= video.duration; t += interval) {
|
|
246
|
+
const frame = await captureFrameAtTime(video, t);
|
|
247
|
+
if (frame) addFrame(frame);
|
|
248
|
+
}
|
|
249
|
+
batchStartBtn.disabled = false;
|
|
250
|
+
batchLabelEl.textContent = labels.batchStart;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
downloadAllBtn.addEventListener('click', () => {
|
|
254
|
+
captured.forEach((frame, i) => {
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
const a = document.createElement('a');
|
|
257
|
+
a.href = frame.url;
|
|
258
|
+
a.download = `frame_${i}.webp`;
|
|
259
|
+
a.click();
|
|
260
|
+
}, i * 200);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
resetBtn.addEventListener('click', () => {
|
|
265
|
+
video.pause();
|
|
266
|
+
video.src = '';
|
|
267
|
+
playerArea.classList.add('vfe-hidden');
|
|
268
|
+
uploadBox.classList.remove('vfe-hidden');
|
|
269
|
+
captured = [];
|
|
270
|
+
framesGallery.innerHTML = '';
|
|
271
|
+
framesGallery.appendChild(galleryEmpty);
|
|
272
|
+
galleryEmpty.classList.remove('vfe-hidden');
|
|
273
|
+
downloadAllBtn.classList.add('vfe-hidden');
|
|
274
|
+
playIcon.classList.remove('vfe-hidden');
|
|
275
|
+
pauseIcon.classList.add('vfe-hidden');
|
|
276
|
+
playText.textContent = labels.playLabel;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
document.getElementById('vfe-lightbox-close')!.addEventListener('click', () => lightbox.classList.remove('vfe-lightbox-open'));
|
|
280
|
+
lightbox.addEventListener('click', (e) => { if (e.target === lightbox) lightbox.classList.remove('vfe-lightbox-open'); });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
init();
|
|
284
|
+
document.addEventListener('astro:page-load', init);
|
|
285
|
+
</script>
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { VideoFrameExtractorUI, VideoFrameExtractorLocaleContent } from '../index';
|
|
3
|
+
|
|
4
|
+
const slug = 'online-video-frame-extractor-capture-hd-stills';
|
|
5
|
+
const title = 'Video Frame Extractor - Capture high-resolution stills';
|
|
6
|
+
const description = 'Extract individual images from your videos with frame-perfect precision. Capture perfect moments in HD locally and for free.';
|
|
7
|
+
|
|
8
|
+
const ui: VideoFrameExtractorUI = {
|
|
9
|
+
uploadTitle: "Upload a video file",
|
|
10
|
+
uploadFormats: "MP4, WebM, MOV, or MKV (Max. 500MB)",
|
|
11
|
+
privacyNote: "The video is not uploaded to the Internet, it is processed in your browser.",
|
|
12
|
+
playLabel: "Play",
|
|
13
|
+
pauseLabel: "Pause",
|
|
14
|
+
captureBtn: "Capture Frame",
|
|
15
|
+
prevFrame: "-1F",
|
|
16
|
+
nextFrame: "+1F",
|
|
17
|
+
batchTitle: "Automatic Extraction",
|
|
18
|
+
batchEvery: "Every",
|
|
19
|
+
batchStart: "Start Sequence",
|
|
20
|
+
batchProcessing: "Extracting...",
|
|
21
|
+
galleryTitle: "Captured Frames",
|
|
22
|
+
galleryEmpty: "Captures will appear here as you take them.",
|
|
23
|
+
downloadAll: "Download All",
|
|
24
|
+
downloadHD: "Download HD Image",
|
|
25
|
+
resetBtn: "Upload another video"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const faq: VideoFrameExtractorLocaleContent['faq'] = [
|
|
29
|
+
{
|
|
30
|
+
question: "Can I extract frames from long videos?",
|
|
31
|
+
answer: "Yes, as long as your browser has enough RAM to load the video. We recommend files up to 500MB for optimal performance.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
question: "In what resolution are the captures saved?",
|
|
35
|
+
answer: "Captures are made at the original video's native resolution. If your video is 4K, you will get a high-quality 4K image.",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const howTo: VideoFrameExtractorLocaleContent['howTo'] = [
|
|
40
|
+
{
|
|
41
|
+
name: "Upload your video",
|
|
42
|
+
text: "Select the video file from your device. We will not upload it to any server.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "Navigate to the exact moment",
|
|
46
|
+
text: "Use the timeline bar or the ±1 frame buttons for surgical precision.",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "Capture the frame",
|
|
50
|
+
text: "Press the capture button to save the moment to the gallery below.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "Download in high quality",
|
|
54
|
+
text: "Download individual captures or the entire session in optimized WebP format.",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const bibliography: VideoFrameExtractorLocaleContent['bibliography'] = [
|
|
59
|
+
{
|
|
60
|
+
name: "Capturing frames with HTML5 Video API",
|
|
61
|
+
url: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const seo: VideoFrameExtractorLocaleContent['seo'] = [
|
|
66
|
+
{
|
|
67
|
+
type: 'summary',
|
|
68
|
+
title: 'Professional Video Frame Extraction',
|
|
69
|
+
items: [
|
|
70
|
+
'Single frame precision (±1 frame) for perfect capture',
|
|
71
|
+
'Supports MP4, WebM, MOV, MKV up to 500MB',
|
|
72
|
+
'Native video resolution preserved (SD, HD, 4K)',
|
|
73
|
+
'Automatic batch extraction at custom intervals'
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{ type: 'title', text: 'Frame Extraction: Freezing Time in Video', level: 2 },
|
|
77
|
+
{ type: 'paragraph', html: 'Sometimes a picture is worth a thousand words. But finding that perfect image within a 10-minute video can be frustrating. Our tool uses your browser\'s local power to extract precision frames without needing professional software.' },
|
|
78
|
+
|
|
79
|
+
{ type: 'stats', items: [
|
|
80
|
+
{ value: '±1', label: 'Single Frame Precision', icon: 'mdi:target' },
|
|
81
|
+
{ value: '100%', label: 'Native Resolution', icon: 'mdi:video-high-definition' },
|
|
82
|
+
{ value: '500MB', label: 'Supported Files', icon: 'mdi:file-video' }
|
|
83
|
+
], columns: 3 },
|
|
84
|
+
|
|
85
|
+
{ type: 'title', text: 'Professional Use Cases', level: 3 },
|
|
86
|
+
{ type: 'comparative', items: [
|
|
87
|
+
{
|
|
88
|
+
title: 'Cinema and Photography',
|
|
89
|
+
description: 'Capture frames as visual reference or composition',
|
|
90
|
+
icon: 'mdi:film',
|
|
91
|
+
points: [
|
|
92
|
+
'Extract stills for movie marketing',
|
|
93
|
+
'Scene composition references',
|
|
94
|
+
'Frame-by-frame analysis'
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
title: 'Digital Content',
|
|
99
|
+
description: 'Create thumbnails and covers for social media',
|
|
100
|
+
icon: 'mdi:youtube',
|
|
101
|
+
points: [
|
|
102
|
+
'High-resolution YouTube thumbnails',
|
|
103
|
+
'Social media covers',
|
|
104
|
+
'Thumbs for presentations'
|
|
105
|
+
],
|
|
106
|
+
highlight: true
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
title: 'Technical Documentation',
|
|
110
|
+
description: 'Extract frames from tutorials and demonstrations',
|
|
111
|
+
icon: 'mdi:book-open',
|
|
112
|
+
points: [
|
|
113
|
+
'Screenshots from tutorial videos',
|
|
114
|
+
'Step-by-step visual documentation',
|
|
115
|
+
'Real-time motion analysis'
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
title: 'Sports and Action',
|
|
120
|
+
description: 'Capture the precise instant of maximum action',
|
|
121
|
+
icon: 'mdi:dumbbell',
|
|
122
|
+
points: [
|
|
123
|
+
'Frame-by-frame sports technique analysis',
|
|
124
|
+
'Heroic moment capture',
|
|
125
|
+
'Motion study'
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
], columns: 2 },
|
|
129
|
+
|
|
130
|
+
{ type: 'title', text: 'Supported Video Formats', level: 3 },
|
|
131
|
+
{ type: 'table', headers: ['Format', 'Extension', 'Compatibility', 'Notes'], rows: [
|
|
132
|
+
['MPEG-4', 'MP4', 'Universal (100%)', 'Best compression, widely used'],
|
|
133
|
+
['WebM', 'WebM', 'Modern browsers', 'Superior compression, smaller size'],
|
|
134
|
+
['QuickTime', 'MOV', 'Safari, some players', 'Apple standard'],
|
|
135
|
+
['Matroska', 'MKV', 'Modern browsers', 'Flexible container, variable quality']
|
|
136
|
+
] },
|
|
137
|
+
|
|
138
|
+
{ type: 'card', title: 'Single Frame Precision', html: 'Moving one single frame forward or backward (±1 frame) is vital to capture the perfect instant: a jump, a smile, a gesture, a scientific moment. At 24 fps, each frame lasts just 41 milliseconds. Our tool gives you millimetric control.' },
|
|
139
|
+
|
|
140
|
+
{ type: 'proscons', items: [
|
|
141
|
+
{
|
|
142
|
+
pro: 'Total privacy: the video is processed 100% locally in your browser',
|
|
143
|
+
con: 'Limited to available RAM memory size (~500MB recommended)'
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
pro: 'Native resolution preserved: SD, HD, 4K without re-compression',
|
|
147
|
+
con: 'Requires modern browser with HTML5 Video support'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
pro: 'Automatic batch extraction at custom intervals',
|
|
151
|
+
con: 'For advanced editing (trim, cuts), you need a video editor'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
pro: 'Export frames in optimized WebP or uncompressed PNG',
|
|
155
|
+
con: 'One frame at a time (does not export automated GIF sequences)'
|
|
156
|
+
}
|
|
157
|
+
], proTitle: 'Advantages', conTitle: 'Limitations' },
|
|
158
|
+
|
|
159
|
+
{ type: 'diagnostic', variant: 'info', title: 'Resolution and Frame Rate', icon: 'mdi:information', badge: 'Technical', html: 'The final frame resolution depends on the original video. If the video is 4K (3840x2160), you will extract 4K frames. If it is 720p, you will get 720p. No smart upscaling: we preserve the video\'s native information.' },
|
|
160
|
+
|
|
161
|
+
{ type: 'glossary', items: [
|
|
162
|
+
{
|
|
163
|
+
term: 'Frame',
|
|
164
|
+
definition: 'Individual image in a video sequence. A 24 fps video contains 24 frames per second.'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
term: 'FPS (Frames Per Second)',
|
|
168
|
+
definition: 'Frames per second. 24 fps (cinema), 30 fps (web video), 60 fps (smooth video), 120 fps (super slow-mo).'
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
term: 'Video Codec',
|
|
172
|
+
definition: 'Compression algorithm: H.264 (MPEG-4), VP9 (WebM), HEVC. Determines file size and quality.'
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
term: 'Bitrate',
|
|
176
|
+
definition: 'Amount of data processed per second (Mbps). Higher bitrate = higher quality but larger files.'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
term: 'Video Resolution',
|
|
180
|
+
definition: 'Pixel dimensions: 720p (1280x720), 1080p (1920x1080), 4K (3840x2160), 8K (7680x4320).'
|
|
181
|
+
}
|
|
182
|
+
] },
|
|
183
|
+
|
|
184
|
+
{ type: 'message', title: 'Professional Frame Extraction', ariaLabel: 'Technical information about video extraction', html: 'You don\'t need complex online converters or professional software. A perfect frame is just 3 clicks away: upload video, navigate, capture. Total privacy, native resolution, instant download.' },
|
|
185
|
+
|
|
186
|
+
{ type: 'title', text: 'Freezing Video Moments', level: 3 },
|
|
187
|
+
{ type: 'paragraph', html: 'Each video contains hundreds of frames. Many of them are pure gold waiting to be discovered. Use this tool to extract those perfect moments without compromising quality or privacy.' }
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
191
|
+
'@context': 'https://schema.org',
|
|
192
|
+
'@type': 'FAQPage',
|
|
193
|
+
mainEntity: faq.map((item) => ({
|
|
194
|
+
'@type': 'Question',
|
|
195
|
+
name: item.question,
|
|
196
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
197
|
+
})),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const howToSchema: WithContext<HowTo> = {
|
|
201
|
+
'@context': 'https://schema.org',
|
|
202
|
+
'@type': 'HowTo',
|
|
203
|
+
name: title,
|
|
204
|
+
description,
|
|
205
|
+
step: howTo.map((step) => ({
|
|
206
|
+
'@type': 'HowToStep',
|
|
207
|
+
name: step.name,
|
|
208
|
+
text: step.text,
|
|
209
|
+
})),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
213
|
+
'@context': 'https://schema.org',
|
|
214
|
+
'@type': 'SoftwareApplication',
|
|
215
|
+
name: title,
|
|
216
|
+
description,
|
|
217
|
+
applicationCategory: 'UtilitiesApplication',
|
|
218
|
+
operatingSystem: 'Web',
|
|
219
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
220
|
+
inLanguage: 'en',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const content: VideoFrameExtractorLocaleContent = {
|
|
224
|
+
slug,
|
|
225
|
+
title,
|
|
226
|
+
description,
|
|
227
|
+
ui,
|
|
228
|
+
seo,
|
|
229
|
+
faq,
|
|
230
|
+
faqTitle: 'Frequently Asked Questions about Video Frame Extraction',
|
|
231
|
+
bibliography,
|
|
232
|
+
bibliographyTitle: 'Technical Standards for Video Capture',
|
|
233
|
+
howTo,
|
|
234
|
+
schemas: [faqSchema as any, howToSchema as any, appSchema],
|
|
235
|
+
};
|