@jjlmoya/utils-audiovisual 1.18.0 → 1.19.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 +1 -1
- package/src/category/index.ts +2 -0
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/videoMerger/bibliography.astro +17 -0
- package/src/tool/videoMerger/bibliography.ts +16 -0
- package/src/tool/videoMerger/component.astro +400 -0
- package/src/tool/videoMerger/entry.ts +51 -0
- package/src/tool/videoMerger/i18n/de.ts +205 -0
- package/src/tool/videoMerger/i18n/en.ts +205 -0
- package/src/tool/videoMerger/i18n/es.ts +205 -0
- package/src/tool/videoMerger/i18n/fr.ts +205 -0
- package/src/tool/videoMerger/i18n/id.ts +205 -0
- package/src/tool/videoMerger/i18n/it.ts +205 -0
- package/src/tool/videoMerger/i18n/ja.ts +205 -0
- package/src/tool/videoMerger/i18n/ko.ts +205 -0
- package/src/tool/videoMerger/i18n/nl.ts +205 -0
- package/src/tool/videoMerger/i18n/pl.ts +205 -0
- package/src/tool/videoMerger/i18n/pt.ts +205 -0
- package/src/tool/videoMerger/i18n/ru.ts +205 -0
- package/src/tool/videoMerger/i18n/sv.ts +205 -0
- package/src/tool/videoMerger/i18n/tr.ts +205 -0
- package/src/tool/videoMerger/i18n/zh.ts +205 -0
- package/src/tool/videoMerger/index.ts +11 -0
- package/src/tool/videoMerger/logic.ts +263 -0
- package/src/tool/videoMerger/online-video-merger.css +440 -0
- package/src/tool/videoMerger/seo.astro +15 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { imageCompressor } from '../tool/imageCompressor/index';
|
|
|
10
10
|
import { collageMaker } from '../tool/collageMaker/index';
|
|
11
11
|
import { videoFrameExtractor } from '../tool/videoFrameExtractor/index';
|
|
12
12
|
import { depthOfFieldCalculator } from '../tool/depthOfFieldCalculator/index';
|
|
13
|
+
import { videoMerger } from '../tool/videoMerger/index';
|
|
13
14
|
|
|
14
15
|
export const audiovisualCategory: AudiovisualCategoryEntry = {
|
|
15
16
|
icon: 'mdi:camera-iris',
|
|
@@ -25,6 +26,7 @@ export const audiovisualCategory: AudiovisualCategoryEntry = {
|
|
|
25
26
|
collageMaker as AudiovisualToolEntry,
|
|
26
27
|
videoFrameExtractor as AudiovisualToolEntry,
|
|
27
28
|
depthOfFieldCalculator as AudiovisualToolEntry,
|
|
29
|
+
videoMerger as AudiovisualToolEntry,
|
|
28
30
|
],
|
|
29
31
|
i18n: {
|
|
30
32
|
es: async () => (await import('./i18n/es')).content,
|
package/src/entries.ts
CHANGED
|
@@ -20,6 +20,8 @@ export { videoFrameExtractor } from './tool/videoFrameExtractor/entry';
|
|
|
20
20
|
export type { VideoFrameExtractorUI, VideoFrameExtractorLocaleContent } from './tool/videoFrameExtractor/entry';
|
|
21
21
|
export { depthOfFieldCalculator } from './tool/depthOfFieldCalculator/entry';
|
|
22
22
|
export type { DepthOfFieldUI, DepthOfFieldLocaleContent } from './tool/depthOfFieldCalculator/entry';
|
|
23
|
+
export { videoMerger } from './tool/videoMerger/entry';
|
|
24
|
+
export type { VideoMergerUI, VideoMergerLocaleContent } from './tool/videoMerger/entry';
|
|
23
25
|
export { audiovisualCategory, toolsCategory } from './category';
|
|
24
26
|
import { chromaticLens } from './tool/chromaticLens/entry';
|
|
25
27
|
import { collageMaker } from './tool/collageMaker/entry';
|
|
@@ -32,4 +34,5 @@ import { timelapseCalculator } from './tool/timelapseCalculator/entry';
|
|
|
32
34
|
import { tvDistance } from './tool/tvDistance/entry';
|
|
33
35
|
import { videoFrameExtractor } from './tool/videoFrameExtractor/entry';
|
|
34
36
|
import { depthOfFieldCalculator } from './tool/depthOfFieldCalculator/entry';
|
|
35
|
-
|
|
37
|
+
import { videoMerger } from './tool/videoMerger/entry';
|
|
38
|
+
export const ALL_ENTRIES = [chromaticLens, collageMaker, exifCleaner, imageCompressor, printQualityCalculator, privacyBlur, subtitleSync, timelapseCalculator, tvDistance, videoFrameExtractor, depthOfFieldCalculator, videoMerger];
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { ALL_TOOLS } from '../tools';
|
|
3
3
|
|
|
4
4
|
describe('Locale Completeness Validation', () => {
|
|
5
|
-
it('all
|
|
6
|
-
expect(ALL_TOOLS.length).toBe(
|
|
5
|
+
it('all 12 tools registered', () => {
|
|
6
|
+
expect(ALL_TOOLS.length).toBe(12);
|
|
7
7
|
});
|
|
8
8
|
});
|
|
@@ -4,8 +4,8 @@ import { audiovisualCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 12 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(12);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('audiovisualCategory should be defined', () => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { videoMerger } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await videoMerger.i18n[locale]?.();
|
|
12
|
+
if (!content || !content.bibliography || content.bibliography.length === 0) return null;
|
|
13
|
+
|
|
14
|
+
const { bibliography } = content;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<SharedBibliography links={bibliography} />
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: "HTML Canvas CaptureStream API",
|
|
6
|
+
url: "https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: "Web Audio API and AudioContext",
|
|
10
|
+
url: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "MediaRecorder API for Client-side Recording",
|
|
14
|
+
url: "https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder",
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { VideoMergerUI } from './index';
|
|
3
|
+
import './online-video-merger.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: VideoMergerUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="vm-root" id="vm-root" data-ui={JSON.stringify(ui)}>
|
|
13
|
+
<div class="vm-premium-card">
|
|
14
|
+
|
|
15
|
+
<div id="vm-upload-box" class="vm-uploader-box">
|
|
16
|
+
<input type="file" id="vm-file-input" accept="video/*" multiple class="vm-hidden" />
|
|
17
|
+
<div class="vm-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="vm-uploader-text">
|
|
23
|
+
<h3>{ui.uploadTitle}</h3>
|
|
24
|
+
<p>{ui.uploadFormats}</p>
|
|
25
|
+
</div>
|
|
26
|
+
<span class="vm-privacy-note">{ui.privacyNote}</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div id="vm-workspace" class="vm-hidden">
|
|
30
|
+
|
|
31
|
+
<div class="vm-list-section">
|
|
32
|
+
<div class="vm-list-header">
|
|
33
|
+
<h4>{ui.listTitle}</h4>
|
|
34
|
+
<button id="vm-add-more" class="vm-btn-main vm-btn-secondary vm-btn-sm" style="flex:none; width:auto; padding: 0.4rem 0.875rem;">
|
|
35
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
36
|
+
<span>{ui.addMoreBtn}</span>
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div id="vm-list-container" class="vm-list-container">
|
|
41
|
+
<div class="vm-empty-state">{ui.emptyList}</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="vm-controls-glass">
|
|
46
|
+
<h4 class="vm-options-title">{ui.optionsTitle}</h4>
|
|
47
|
+
|
|
48
|
+
<div class="vm-options-grid">
|
|
49
|
+
<div class="vm-select-group">
|
|
50
|
+
<label>{ui.optionResolution}</label>
|
|
51
|
+
<select id="vm-res-select">
|
|
52
|
+
<option value="1920x1080">1920x1080 (Full HD 16:9)</option>
|
|
53
|
+
<option value="1280x720">1280x720 (HD 16:9)</option>
|
|
54
|
+
<option value="3840x2160">3840x2160 (4K UHD 16:9)</option>
|
|
55
|
+
<option value="640x360">640x360 (nHD 16:9)</option>
|
|
56
|
+
<option value="1080x1920">1080x1920 (Vertical 9:16)</option>
|
|
57
|
+
</select>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="vm-select-group">
|
|
61
|
+
<label>{ui.optionFps}</label>
|
|
62
|
+
<select id="vm-fps-select">
|
|
63
|
+
<option value="30">30 FPS</option>
|
|
64
|
+
<option value="24">24 FPS</option>
|
|
65
|
+
<option value="60">60 FPS</option>
|
|
66
|
+
</select>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<p class="vm-quality-note">{ui.optionsQualityNote}</p>
|
|
71
|
+
|
|
72
|
+
<div id="vm-resolution-warning" class="vm-warning-box vm-hidden">
|
|
73
|
+
<svg viewBox="0 0 24 24" fill="currentColor" style="width: 1.25rem; height: 1.25rem; flex-shrink: 0;"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
|
|
74
|
+
<span id="vm-resolution-warning-text"></span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div id="vm-progress-wrapper" class="vm-progress-wrapper vm-hidden" style="margin-top: 1.25rem;">
|
|
79
|
+
<div class="vm-progress-header">
|
|
80
|
+
<span id="vm-status-label">{ui.mergingStatus}</span>
|
|
81
|
+
<span id="vm-progress-percent">0%</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="vm-progress-bar">
|
|
84
|
+
<div id="vm-progress-fill" class="vm-progress-fill"></div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="vm-actions-row" style="margin-top: 1.5rem;">
|
|
89
|
+
<button id="vm-merge-btn" class="vm-btn-main vm-btn-primary">
|
|
90
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
91
|
+
<span id="vm-merge-text">{ui.mergeBtn}</span>
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
<a id="vm-download-btn" href="" download="merged_video.webm" class="vm-btn-main vm-btn-primary vm-hidden" style="background-color: var(--vm-success); box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3);">
|
|
95
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z"/></svg>
|
|
96
|
+
<span>{ui.downloadBtn}</span>
|
|
97
|
+
</a>
|
|
98
|
+
|
|
99
|
+
<button id="vm-reset-btn" class="vm-btn-main vm-btn-secondary" style="flex:none; width:auto;">
|
|
100
|
+
<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>
|
|
101
|
+
<span>{ui.resetBtn}</span>
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
import { VideoMergerEngine, getVideoMetadata, formatTime } from './logic';
|
|
112
|
+
import type { VideoItem } from './logic';
|
|
113
|
+
|
|
114
|
+
function init() {
|
|
115
|
+
const root = document.getElementById('vm-root');
|
|
116
|
+
if (!root) return;
|
|
117
|
+
|
|
118
|
+
const fileInput = root.querySelector('#vm-file-input') as HTMLInputElement;
|
|
119
|
+
const uploadBox = root.querySelector('#vm-upload-box') as HTMLElement;
|
|
120
|
+
const workspace = root.querySelector('#vm-workspace') as HTMLElement;
|
|
121
|
+
const addMoreBtn = root.querySelector('#vm-add-more') as HTMLElement;
|
|
122
|
+
const listContainer = root.querySelector('#vm-list-container') as HTMLElement;
|
|
123
|
+
|
|
124
|
+
const resSelect = root.querySelector('#vm-res-select') as HTMLSelectElement;
|
|
125
|
+
const fpsSelect = root.querySelector('#vm-fps-select') as HTMLSelectElement;
|
|
126
|
+
|
|
127
|
+
const progressWrapper = root.querySelector('#vm-progress-wrapper') as HTMLElement;
|
|
128
|
+
const statusLabel = root.querySelector('#vm-status-label') as HTMLElement;
|
|
129
|
+
const progressPercent = root.querySelector('#vm-progress-percent') as HTMLElement;
|
|
130
|
+
const progressBar = root.querySelector('.vm-progress-bar') as HTMLElement;
|
|
131
|
+
|
|
132
|
+
const mergeBtn = root.querySelector('#vm-merge-btn') as HTMLButtonElement;
|
|
133
|
+
const downloadBtn = root.querySelector('#vm-download-btn') as HTMLAnchorElement;
|
|
134
|
+
const resetBtn = root.querySelector('#vm-reset-btn') as HTMLButtonElement;
|
|
135
|
+
const resolutionWarning = root.querySelector('#vm-resolution-warning') as HTMLElement;
|
|
136
|
+
const resolutionWarningText = root.querySelector('#vm-resolution-warning-text') as HTMLElement;
|
|
137
|
+
|
|
138
|
+
let items: VideoItem[] = [];
|
|
139
|
+
const engine = new VideoMergerEngine();
|
|
140
|
+
|
|
141
|
+
function checkResolutions() {
|
|
142
|
+
const resParts = resSelect.value.split('x');
|
|
143
|
+
const width = parseInt(resParts[0] || '1920');
|
|
144
|
+
const height = parseInt(resParts[1] || '1080');
|
|
145
|
+
|
|
146
|
+
let hasMismatch = false;
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
if (item.width !== width || item.height !== height) {
|
|
149
|
+
hasMismatch = true;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (hasMismatch) {
|
|
155
|
+
resolutionWarning.classList.remove('vm-hidden');
|
|
156
|
+
resolutionWarningText.textContent = root!.dataset.ui ? JSON.parse(root!.dataset.ui).resolutionWarning : 'Advertencia: Resoluciones distintas';
|
|
157
|
+
} else {
|
|
158
|
+
resolutionWarning.classList.add('vm-hidden');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
resSelect.onchange = () => {
|
|
163
|
+
checkResolutions();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
function renderList() {
|
|
167
|
+
listContainer.innerHTML = '';
|
|
168
|
+
if (items.length === 0) {
|
|
169
|
+
workspace.classList.add('vm-hidden');
|
|
170
|
+
uploadBox.classList.remove('vm-hidden');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
mergeBtn.disabled = items.length < 2;
|
|
175
|
+
checkResolutions();
|
|
176
|
+
|
|
177
|
+
items.forEach((item, index) => {
|
|
178
|
+
const card = document.createElement('div');
|
|
179
|
+
card.className = 'vm-item-card';
|
|
180
|
+
card.innerHTML = `
|
|
181
|
+
<span class="vm-item-index">${index + 1}</span>
|
|
182
|
+
<div class="vm-item-details">
|
|
183
|
+
<h5 class="vm-item-name">${item.name}</h5>
|
|
184
|
+
<div class="vm-item-meta">
|
|
185
|
+
<span>${formatTime(item.duration)}</span>
|
|
186
|
+
<span>${item.width} × ${item.height}</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="vm-item-controls">
|
|
190
|
+
<button class="vm-btn-icon vm-move-up" ${index === 0 ? 'disabled' : ''}>
|
|
191
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>
|
|
192
|
+
</button>
|
|
193
|
+
<button class="vm-btn-icon vm-move-down" ${index === items.length - 1 ? 'disabled' : ''}>
|
|
194
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
|
|
195
|
+
</button>
|
|
196
|
+
<button class="vm-btn-icon vm-danger vm-delete">
|
|
197
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
const moveUp = card.querySelector('.vm-move-up') as HTMLElement;
|
|
203
|
+
const moveDown = card.querySelector('.vm-move-down') as HTMLElement;
|
|
204
|
+
const delBtn = card.querySelector('.vm-delete') as HTMLElement;
|
|
205
|
+
|
|
206
|
+
moveUp.onclick = () => {
|
|
207
|
+
if (index > 0) {
|
|
208
|
+
const temp = items[index];
|
|
209
|
+
const prev = items[index - 1];
|
|
210
|
+
if (temp && prev) {
|
|
211
|
+
items[index] = prev;
|
|
212
|
+
items[index - 1] = temp;
|
|
213
|
+
renderList();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
moveDown.onclick = () => {
|
|
219
|
+
if (index < items.length - 1) {
|
|
220
|
+
const temp = items[index];
|
|
221
|
+
const next = items[index + 1];
|
|
222
|
+
if (temp && next) {
|
|
223
|
+
items[index] = next;
|
|
224
|
+
items[index + 1] = temp;
|
|
225
|
+
renderList();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
delBtn.onclick = () => {
|
|
231
|
+
URL.revokeObjectURL(item.url);
|
|
232
|
+
items.splice(index, 1);
|
|
233
|
+
renderList();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
listContainer.appendChild(card);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function handleFiles(files: FileList) {
|
|
241
|
+
uploadBox.classList.add('vm-hidden');
|
|
242
|
+
workspace.classList.remove('vm-hidden');
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < files.length; i++) {
|
|
245
|
+
const file = files[i];
|
|
246
|
+
if (!file) continue;
|
|
247
|
+
try {
|
|
248
|
+
const meta = await getVideoMetadata(file);
|
|
249
|
+
items.push({
|
|
250
|
+
id: Math.random().toString(36).substring(2, 9),
|
|
251
|
+
file,
|
|
252
|
+
url: URL.createObjectURL(file),
|
|
253
|
+
name: file.name,
|
|
254
|
+
duration: meta.duration,
|
|
255
|
+
width: meta.width,
|
|
256
|
+
height: meta.height
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (items.length === 1) {
|
|
260
|
+
const value = `${meta.width}x${meta.height}`;
|
|
261
|
+
const exists = Array.from(resSelect.options).some(opt => opt.value === value);
|
|
262
|
+
if (!exists) {
|
|
263
|
+
const opt = document.createElement('option');
|
|
264
|
+
opt.value = value;
|
|
265
|
+
opt.textContent = `${meta.width}×${meta.height} (Original)`;
|
|
266
|
+
resSelect.appendChild(opt);
|
|
267
|
+
}
|
|
268
|
+
resSelect.value = value;
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.error(e);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
renderList();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
uploadBox.onclick = () => fileInput.click();
|
|
278
|
+
fileInput.onchange = () => {
|
|
279
|
+
if (fileInput.files) {
|
|
280
|
+
handleFiles(fileInput.files);
|
|
281
|
+
fileInput.value = '';
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
addMoreBtn.onclick = () => fileInput.click();
|
|
286
|
+
|
|
287
|
+
root.ondragover = (e) => { e.preventDefault(); uploadBox.classList.add('vm-dragover'); };
|
|
288
|
+
root.ondragleave = () => uploadBox.classList.remove('vm-dragover');
|
|
289
|
+
root.ondrop = (e) => {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
uploadBox.classList.remove('vm-dragover');
|
|
292
|
+
if (e.dataTransfer?.files) handleFiles(e.dataTransfer.files);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
mergeBtn.onclick = async () => {
|
|
296
|
+
if (items.length < 2) return;
|
|
297
|
+
|
|
298
|
+
const resParts = resSelect.value.split('x');
|
|
299
|
+
const width = parseInt(resParts[0] || '1920');
|
|
300
|
+
const height = parseInt(resParts[1] || '1080');
|
|
301
|
+
const fps = parseInt(fpsSelect.value);
|
|
302
|
+
|
|
303
|
+
mergeBtn.disabled = true;
|
|
304
|
+
resetBtn.disabled = true;
|
|
305
|
+
addMoreBtn.classList.add('vm-hidden');
|
|
306
|
+
|
|
307
|
+
const controlButtons = listContainer.querySelectorAll('.vm-btn-icon') as NodeListOf<HTMLButtonElement>;
|
|
308
|
+
controlButtons.forEach(btn => btn.disabled = true);
|
|
309
|
+
|
|
310
|
+
progressWrapper.classList.remove('vm-hidden');
|
|
311
|
+
downloadBtn.classList.add('vm-hidden');
|
|
312
|
+
|
|
313
|
+
engine.setItems(items);
|
|
314
|
+
|
|
315
|
+
progressBar.innerHTML = '';
|
|
316
|
+
const numVideos = items.length;
|
|
317
|
+
const segmentFills: HTMLElement[] = [];
|
|
318
|
+
for (let k = 0; k < numVideos; k++) {
|
|
319
|
+
const segment = document.createElement('div');
|
|
320
|
+
segment.className = 'vm-progress-segment';
|
|
321
|
+
const fill = document.createElement('div');
|
|
322
|
+
fill.className = 'vm-progress-segment-fill';
|
|
323
|
+
segment.appendChild(fill);
|
|
324
|
+
progressBar.appendChild(segment);
|
|
325
|
+
segmentFills.push(fill);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const blob = await engine.merge({ width, height, fps }, (progress, name, index) => {
|
|
330
|
+
const globalProgress = Math.floor(((index * 100) + progress) / numVideos);
|
|
331
|
+
progressPercent.textContent = `${globalProgress}%`;
|
|
332
|
+
|
|
333
|
+
for (let k = 0; k < numVideos; k++) {
|
|
334
|
+
const fill = segmentFills[k];
|
|
335
|
+
if (fill) {
|
|
336
|
+
if (k < index) {
|
|
337
|
+
fill.style.width = '100%';
|
|
338
|
+
} else if (k === index) {
|
|
339
|
+
fill.style.width = `${progress}%`;
|
|
340
|
+
} else {
|
|
341
|
+
fill.style.width = '0%';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
statusLabel.textContent = `${root.dataset.ui ? JSON.parse(root.dataset.ui).mergingStatus : 'Uniendo...'} (${name})`;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const resultUrl = URL.createObjectURL(blob);
|
|
350
|
+
|
|
351
|
+
progressPercent.textContent = '100%';
|
|
352
|
+
for (let k = 0; k < numVideos; k++) {
|
|
353
|
+
const fill = segmentFills[k];
|
|
354
|
+
if (fill) fill.style.width = '100%';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
downloadBtn.href = resultUrl;
|
|
358
|
+
downloadBtn.classList.remove('vm-hidden');
|
|
359
|
+
mergeBtn.classList.add('vm-hidden');
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(err);
|
|
362
|
+
} finally {
|
|
363
|
+
resetBtn.disabled = false;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
resetBtn.onclick = () => {
|
|
368
|
+
items.forEach(item => URL.revokeObjectURL(item.url));
|
|
369
|
+
items = [];
|
|
370
|
+
|
|
371
|
+
const downloadUrl = downloadBtn.href;
|
|
372
|
+
if (downloadUrl) URL.revokeObjectURL(downloadUrl);
|
|
373
|
+
|
|
374
|
+
progressWrapper.classList.add('vm-hidden');
|
|
375
|
+
progressPercent.textContent = '0%';
|
|
376
|
+
progressBar.innerHTML = '<div id="vm-progress-fill" class="vm-progress-fill"></div>';
|
|
377
|
+
|
|
378
|
+
mergeBtn.disabled = false;
|
|
379
|
+
mergeBtn.classList.remove('vm-hidden');
|
|
380
|
+
downloadBtn.classList.add('vm-hidden');
|
|
381
|
+
addMoreBtn.classList.remove('vm-hidden');
|
|
382
|
+
|
|
383
|
+
resSelect.value = '1920x1080';
|
|
384
|
+
const defaultValues = ['1920x1080', '1280x720', '3840x2160', '640x360', '1080x1920'];
|
|
385
|
+
for (let i = resSelect.options.length - 1; i >= 0; i--) {
|
|
386
|
+
const val = resSelect.options[i]?.value;
|
|
387
|
+
if (val && !defaultValues.includes(val)) {
|
|
388
|
+
resSelect.remove(i);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
resolutionWarning.classList.add('vm-hidden');
|
|
392
|
+
|
|
393
|
+
fileInput.value = '';
|
|
394
|
+
renderList();
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
init();
|
|
399
|
+
document.addEventListener('astro:page-load', init);
|
|
400
|
+
</script>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AudiovisualToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export interface VideoMergerUI {
|
|
4
|
+
uploadTitle: string;
|
|
5
|
+
uploadFormats: string;
|
|
6
|
+
privacyNote: string;
|
|
7
|
+
addMoreBtn: string;
|
|
8
|
+
mergeBtn: string;
|
|
9
|
+
mergingStatus: string;
|
|
10
|
+
downloadBtn: string;
|
|
11
|
+
resetBtn: string;
|
|
12
|
+
emptyList: string;
|
|
13
|
+
listTitle: string;
|
|
14
|
+
optionsTitle: string;
|
|
15
|
+
optionResolution: string;
|
|
16
|
+
optionFps: string;
|
|
17
|
+
optionsQualityNote: string;
|
|
18
|
+
faqTitle: string;
|
|
19
|
+
bibliographyTitle: string;
|
|
20
|
+
resolutionWarning: string;
|
|
21
|
+
[key: string]: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type VideoMergerLocaleContent = ToolLocaleContent<VideoMergerUI>;
|
|
25
|
+
|
|
26
|
+
export const videoMerger: AudiovisualToolEntry<VideoMergerUI> = {
|
|
27
|
+
id: 'online-video-merger',
|
|
28
|
+
icons: {
|
|
29
|
+
bg: 'mdi:video-vintage',
|
|
30
|
+
fg: 'mdi:video-plus',
|
|
31
|
+
},
|
|
32
|
+
i18n: {
|
|
33
|
+
de: async () => (await import('./i18n/de')).content as unknown as VideoMergerLocaleContent,
|
|
34
|
+
en: async () => (await import('./i18n/en')).content as unknown as VideoMergerLocaleContent,
|
|
35
|
+
es: async () => (await import('./i18n/es')).content as unknown as VideoMergerLocaleContent,
|
|
36
|
+
fr: async () => (await import('./i18n/fr')).content as unknown as VideoMergerLocaleContent,
|
|
37
|
+
id: async () => (await import('./i18n/id')).content as unknown as VideoMergerLocaleContent,
|
|
38
|
+
it: async () => (await import('./i18n/it')).content as unknown as VideoMergerLocaleContent,
|
|
39
|
+
ja: async () => (await import('./i18n/ja')).content as unknown as VideoMergerLocaleContent,
|
|
40
|
+
ko: async () => (await import('./i18n/ko')).content as unknown as VideoMergerLocaleContent,
|
|
41
|
+
nl: async () => (await import('./i18n/nl')).content as unknown as VideoMergerLocaleContent,
|
|
42
|
+
pl: async () => (await import('./i18n/pl')).content as unknown as VideoMergerLocaleContent,
|
|
43
|
+
pt: async () => (await import('./i18n/pt')).content as unknown as VideoMergerLocaleContent,
|
|
44
|
+
ru: async () => (await import('./i18n/ru')).content as unknown as VideoMergerLocaleContent,
|
|
45
|
+
sv: async () => (await import('./i18n/sv')).content as unknown as VideoMergerLocaleContent,
|
|
46
|
+
tr: async () => (await import('./i18n/tr')).content as unknown as VideoMergerLocaleContent,
|
|
47
|
+
zh: async () => (await import('./i18n/zh')).content as unknown as VideoMergerLocaleContent,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { bibliography } from './bibliography';
|