forked from wiwikuan/fast-srt-subtitle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.js
487 lines (409 loc) · 16.3 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
const SRT_ID = 'srtFile';
const VIDEO_ID = 'videoFile';
const srtInput = document.querySelector('#srtFile');
const videoInput = document.querySelector('#videoFile');
const video = document.querySelector('#video');
const textArea = document.querySelector('#textArea');
const status = document.querySelector('#status');
let reactTime = 0;
let IsAutoJump = 1;
let subTexts = [];
let currentStamping = 0;
let lines = [];
function clamp(num) {
return Math.max(num, 0);
}
const keyMap = {
'k': video => {
Kkeyfunction(video, reactTime);
},
'l': video => {
Lkeyfunction(video, reactTime);
},
'i': () => {
if (currentStamping != 0) {
currentStamping -= 1;
status.textContent = getCurrentStatus();
}
},
'o': () => {
currentStamping += 1;
status.textContent = getCurrentStatus();
},
'u': () => (video.currentTime -= 2),
'p': () => (video.currentTime += 2),
'q': () => makeSRT(),
' ': () => PlayORPause()
};
function getCurrentStatus() {
return `Stamping Line ${currentStamping} | Playhead: ${video.currentTime}`;
}
var IsHold = 0;
var IsAllowRepeatKey = { 'i': true, 'o': true, 'u': true, 'p': true };
function execHotkey(keyMap) {
document.addEventListener('keypress', function (e) {
if (IsInput !== 0 || IsHold !== 0) { //IsInput跟IsHold同時等於0時,才會往下執行,防止輸入字幕時觸發鍵盤事件,與按住鍵盤時連續觸發
return;
}
const execFn = keyMap[e.key.toLowerCase()];
if (typeof execFn === 'function') {
execFn(video);
updateContent();
}
if (!IsAllowRepeatKey[e.key.toLowerCase()]) { //不允許連按的按鍵IsHold會+1,防止連續觸發,允許連按的按鍵IsHold不會變,所以依然會連續觸發
IsHold += 1;
}
});
}
document.addEventListener('keyup', function () {
IsHold = 0;
});
function updateContent() {
const head = '** 目前 ---> ';
const content = subTexts
.slice(currentStamping, currentStamping + 5)
.map((text, i) => {
const [timeStart, timeEnd] = lines[currentStamping + i];
return `${i === 0 ? head : ''}${text} | ${timeStart} --> ${timeEnd}`;
})
.join('\n');
textArea.value = content;
}
function handleFileUpload(e) {
if (e.target.files !== null) {
const reader = new FileReader();
const file = e.target.files[0];
const currentFileName = file.name;
const currentFileExtension = currentFileName.split(".").pop(); //取得副檔名
/*
if it's srt file, fill text area with srt content
if it's video, load it into video tag
*/
reader.onload = function () {
if (e.target.id === SRT_ID) {
if (currentFileExtension === 'srt') { //如果匯入的是srt檔可以自動解析時間
let srtFileTexts = reader.result.split('\n');
srtFileTexts = srtFileTexts.filter(function (item) { //自動刪除空白行
return item != "" && item != "\r";
});
let srtLength = srtFileTexts.length / 3;
for (let i = 0; i < srtLength; i++) {
subTexts[i] = srtFileTexts[i * 3 + 2];
subTexts[i] = subTexts[i].split('\r').shift() + "\r"; //統一為沒有換行的字幕加上換行
let startTimeStamp = srtFileTexts[i * 3 + 1].split("-->").shift();
let endTimeStamp = srtFileTexts[i * 3 + 1].split("-->").pop();
let timeStart = 3600 * startTimeStamp.split(':')[0];
timeStart = timeStart + 60 * startTimeStamp.split(':')[1];
timeStart = timeStart + 1 * startTimeStamp.split(':')[2].split(',').shift();
timeStart = timeStart + 0.001 * startTimeStamp.split(':')[2].split(',')[1].split(' ').shift();
let timeEnd = 3600 * endTimeStamp.split(':')[0].split(' ').pop();
timeEnd = timeEnd + 60 * endTimeStamp.split(':')[1];
timeEnd = timeEnd + 1 * endTimeStamp.split(':')[2].split(',').shift();
timeEnd = timeEnd + 0.001 * endTimeStamp.split(':')[2].split(',').pop();
lines[i] = [timeStart, timeEnd];
MakeSub(i);
}
updateContent();
execHotkey(keyMap);
} else {
subTexts = reader.result.split('\n');
subTexts = subTexts.filter(function (item) { //自動刪除空白行
return item != "" && item != "\r";
});
subTexts.forEach((_, i) => (lines[i] = [null, null]));
lines[0][0] = 0;
updateContent();
execHotkey(keyMap);
}
}
};
reader.onerror = function () {
alert('無法讀取檔案!');
};
if (e.target.id === SRT_ID) {
reader.readAsText(file);
} else {
video.src = URL.createObjectURL(file);
var objectURL = URL.createObjectURL(file); //載入影片時使wavesurfer也載入影片
wavesurfer.load(objectURL);
}
}
document.getElementById('video').focus();
}
videoInput.addEventListener('change', handleFileUpload);
srtInput.addEventListener('change', handleFileUpload);
video.addEventListener('timeupdate', function (e) {
status.textContent = getCurrentStatus();
wavesurfer.seekTo(video.currentTime / wavesurfer.getDuration()); //影片時間線更新時也會更新wavesurfer的時間線
});
function makeSRT() {
srt = '';
for (let i = 0; i < subTexts.length; i++) {
// line number
srt += i + 1 + '\n';
// line time
let sh, sm, ss, sms;
let eh, em, es, ems;
const [timeStart, timeEnd] = lines[i];
const leftPad = str => `${str}`.padStart(2, '0');
const leftPad3 = str => `${str}`.padStart(3, '0');
sh = leftPad(Math.floor(timeStart / 3600));
sm = leftPad(Math.floor((timeStart % 3600) / 60));
ss = leftPad(Math.floor(timeStart % 60));
sms = leftPad3(Math.floor((timeStart * 1000) % 1000));
eh = leftPad(Math.floor(timeEnd / 3600));
em = leftPad(Math.floor((timeEnd % 3600) / 60));
es = leftPad(Math.floor(timeEnd % 60));
ems = leftPad3(Math.floor((timeEnd * 1000) % 1000));
srt += `${sh}:${sm}:${ss},${sms} --> ${eh}:${em}:${es},${ems}\n`;
srt += subTexts[i];
srt += '\n\n';
}
console.log(srt);
let blob = new Blob([srt], {
type: 'text/plain;charset=utf-8'
});
const a = document.createElement('a');
const file = new Blob([srt], { type: 'text/plain;charset=utf-8' });
a.href = URL.createObjectURL(file);
a.download = 'srt.txt';
a.click();
URL.revokeObjectURL(a.href);
a.remove();
}
document.getElementById('MakeSrtBtn').addEventListener('click', function () {
makeSRT();
document.getElementById('video').focus();
});
function PlayORPause() {
if (document.activeElement !== document.getElementById('video')) {
if (video.paused === true) {
video.play();
} else {
video.pause();
}
}
}
var wavesurfer = WaveSurfer.create({
container: '#waveform',
barWidth: 1,
height: 100 //默認128
});
var pxPerSec = 40;
wavesurfer.on('ready', function () {
wavesurfer.zoom(pxPerSec); //根據pxPerSec縮放波形圖,pxPerSec代表每秒有幾像素,越大越寬
wavesurfer.setMute(true); //波形圖本身也是一個聲音的撥放器因此把聲音靜音
document.getElementById('subtainer').style.width = Math.round(pxPerSec * wavesurfer.getDuration()) + 'px'; //載入完成後改變字幕軸寬度使之與波形圖一致
});
function UpdateLoadingFlag(Percentage) {
if (document.getElementById("status")) {
document.getElementById("status").innerText = "Loading " + Percentage + "%";
}
}
wavesurfer.on('loading', function (Percentage) { //波形圖載入時在status顯示載入百分比
UpdateLoadingFlag(Percentage);
});
video.addEventListener('play', function () {
wavesurfer.play();
});
video.addEventListener('pause', function () {
wavesurfer.pause();
wavesurfer.seekTo(video.currentTime / wavesurfer.getDuration());
});
wavesurfer.on('seek', function (seekprogress) { //這裡用比較奇怪的方法完成波形圖與影片時間線的同步,要解決這個問題就不能使用瀏覽器內建的影片撥放控制器,要自制撥放、暫停...等等的按鈕,先這樣應急
if (video.paused === true && Math.abs(video.currentTime - seekprogress * wavesurfer.getDuration()) > 0.00001) { //影片暫停時才能精細的調整時間,但時間差距不能小於0.00001秒,以免無限的迴圈
video.currentTime = seekprogress * wavesurfer.getDuration();
}
if (Math.abs(video.currentTime - seekprogress * wavesurfer.getDuration()) > 0.3) { //當點擊波形圖更改時間線時,時間差距要大於0.3秒才會觸發,並且影片會暫停,影片播放時也會更新時間線所以設定0.3秒做為門檻
video.pause();
video.currentTime = seekprogress * wavesurfer.getDuration();
}
});
document.getElementById("checkbox").addEventListener('change', function () {
if (document.getElementById("checkbox").checked) {
reactTime = 0.4;
} else {
reactTime = 0;
}
});
document.getElementById("autoJumpCheckbox").addEventListener('change', function () {
if (document.getElementById("autoJumpCheckbox").checked) {
IsAutoJump = 1;
} else {
IsAutoJump = 0;
}
});
wavesurfer.on('scroll', function (e) {
document.getElementById('subbox').scrollLeft = e.target.scrollLeft;
});
var SubWidth = [];
var SubLeft = [];
function MakeSub(SubSequence) {
if (SubSequence < 0) {
return;
}
if ((lines[SubSequence][0] !== null) && (lines[SubSequence][1] !== null)) {
if (document.getElementById('sub' + (SubSequence)) !== null) { //新增字幕條時如果已經存在舊的字幕條將刪除舊字幕條
document.getElementById('sub' + (SubSequence)).remove();
}
SubWidth[SubSequence] = pxPerSec * (lines[SubSequence][1] - lines[SubSequence][0]);
SubLeft[SubSequence] = pxPerSec * lines[SubSequence][0];
var div = document.createElement("div");
div.setAttribute('id', 'sub' + (SubSequence));
div.style.overflow = "hidden";
div.style.fontSize = "100%";
div.style.background = "Cyan";
div.style.height = "100%";
div.style.position = "absolute";
div.style.border = "1px #000000 solid";
div.style.userSelect = "none";
div.style.left = pxPerSec * lines[SubSequence][0] + "px";
div.style.width = pxPerSec * (lines[SubSequence][1] - lines[SubSequence][0]) + "px";
div.innerHTML = '<div class="subleft"></div><div class="subright"></div>' + subTexts[SubSequence];
document.getElementById("subtainer").appendChild(div);
}
}
function Kkeyfunction(video, reactTime = 0) {
if (currentStamping >= lines.length) {
return;
}
if (currentStamping !== lines.length - 1) { //幫最後一行上時間標時,防止出現錯誤
lines[currentStamping + 1][0] = clamp(video.currentTime - reactTime);
}
if (currentStamping >= 0) {
if (lines[currentStamping][1] > video.currentTime - reactTime || lines[currentStamping][1] === null) {
lines[currentStamping][1] = clamp(video.currentTime - 0.03 - reactTime);
}
}
MakeSub(currentStamping);
if (currentStamping !== lines.length - 1) {
MakeSub(currentStamping + 1);
currentStamping += 1;
}
}
function Lkeyfunction(video, reactTime = 0) {
lines[currentStamping] = [
lines[currentStamping][0],
video.currentTime - reactTime
];
if ((currentStamping !== lines.length - 1) && lines[currentStamping][1] > lines[currentStamping + 1][0] && (lines[currentStamping + 1][0] !== null)) {
lines[currentStamping + 1][0] = clamp(lines[currentStamping][1] + 0.03);
}
MakeSub(currentStamping);
if (currentStamping !== lines.length - 1) {
MakeSub(currentStamping + 1);
}
}
document.getElementById("subtainer").addEventListener('mousedown', function (e) { //用事件代理監聽字幕條兩端的拖動元素,這樣不用註冊很多監聽器,當拖動完成後會更新時間,如同K鍵,L鍵的效果
var OmouseX = e.pageX;
var PID = e.target.parentNode.id;
var SubSequence = PID.substr(3);
document.getElementById(PID).style.zIndex = 1; //使字幕條拖動時不會被遮擋,當拖動停止後會更新時間並用K鍵,L鍵更新元素,所以新的元素不會有zIndex,以後的元素不會被新元素遮擋
var newWidth;
if (e.target.className.toLowerCase() == 'subleft') {
window.addEventListener('mousemove', Lresize);
window.addEventListener('mouseup', stopLresize);
function Lresize(e) {
document.getElementById(PID).style.width = (SubWidth[SubSequence] - e.pageX + OmouseX) + "px";
document.getElementById(PID).style.left = (SubLeft[SubSequence] + e.pageX - OmouseX) + "px";
}
function stopLresize() {
window.removeEventListener('mousemove', Lresize);
window.removeEventListener('mouseup', stopLresize);
GetnewWidth();
currentStamping = (Number(SubSequence) - 1);
video.currentTime = (SubWidth[SubSequence] - newWidth + SubLeft[SubSequence]) / pxPerSec;
Kkeyfunction(video);
autoJump();
updateContent();
}
}
if (e.target.className.toLowerCase() == 'subright') {
window.addEventListener('mousemove', Rresize);
window.addEventListener('mouseup', stopRresize);
function Rresize(e) {
document.getElementById(PID).style.width = (SubWidth[SubSequence] + e.pageX - OmouseX) + "px";
}
function stopRresize() {
window.removeEventListener('mousemove', Rresize);
window.removeEventListener('mouseup', stopRresize);
GetnewWidth();
currentStamping = Number(SubSequence);
video.currentTime = (newWidth + SubLeft[SubSequence]) / pxPerSec;
Lkeyfunction(video);
autoJump();
updateContent();
}
}
function GetnewWidth() { //當拖動停止時取得目前的新寬度,但是最低不會低於10px
newWidth = Number(document.getElementById(PID).style.width.split('px')[0]);
if (newWidth < 10) {
newWidth = 10;
}
}
});
var IsInput = 0; //檢測現在是否有開啟的輸入框
document.getElementById("subtainer").addEventListener('dblclick', function (e) { //雙擊字幕條本體可以產生輸入框,輸入後的內容會改變subTexts裡的內容,也可以取消
if (IsInput === 0 && e.target.id !== "subtainer") { //目前有輸入框時不會動作,啟動後IsInput會設為1,關閉後IsInput會重新設定為0
IsInput = 1;
var input = document.createElement("input");
var button = document.createElement("button");
var SubSequence = e.target.id.substr(3);
var InputID = e.target.id + 'input';
MakeInput();
MakeBtn();
currentStamping = Number(SubSequence);
updateContent();
document.getElementById(InputID).addEventListener('keydown', PressEnter);
document.getElementById("MakeBtnID").addEventListener('click', PressBtn);
}
function MakeInput() {
createInputAndBtn(input, InputID, SubLeft[SubSequence]);
input.value = subTexts[SubSequence];
document.getElementById(InputID).focus();
}
function MakeBtn() {
createInputAndBtn(button, "MakeBtnID", 170 + SubLeft[SubSequence]); //預設輸入框的長度為170px為了不擋到輸入框將取消鈕距離左邊+170px
button.innerHTML = '取消';
}
function createInputAndBtn(ele, eleID, eleLeft) {
ele.setAttribute('id', eleID);
document.getElementById("subtainer").appendChild(ele);
ele.style.zIndex = 1;
ele.style.position = "absolute";
ele.style.height = "110%";
ele.style.left = eleLeft + "px";
}
function PressEnter(e) {
if (e.keyCode === 13) { // Enter的keyCode是13
removeInputAndBtn();
subTexts[SubSequence] = input.value + '\n';
MakeSub(currentStamping);
autoJump();
updateContent();
}
}
function PressBtn() {
removeInputAndBtn();
autoJump();
updateContent();
}
function removeInputAndBtn() {
document.getElementById("MakeBtnID").removeEventListener('click', PressBtn);
document.getElementById(InputID).removeEventListener('keydown', PressEnter);
document.getElementById("MakeBtnID").remove();
document.getElementById(InputID).remove();
IsInput = 0;
}
});
function autoJump() { //當autoJump開啟時拖動或輸入結束時,自動跳到下一個未上時間標的地方
if (IsAutoJump == 1) {
for (let i = currentStamping; i < lines.length; i++) {
if ((lines[i][0] === null) || (lines[i][1] === null)) {
currentStamping = i;
video.currentTime = lines[i - 1][1] + 0.33; //自動跳轉後撥放頭也會自動跳轉到後面,並且向後一微小的偏移,以方便誤觸後生成的字幕不會太小,方便拖動
break;
}
}
}
}