ในบทความนี้จะมาสอนกระบวนการวิธีทำ Subtitles ให้มี 2 อันเปิดคู่กัน ไปจนสุดท้ายคือมีเขียนโค้ด Javascript อยู่ในส่วนท้ายบทความ
ส่วนถ้าใครหลงเข้ามาเพราะต้องการใช้งาน Multi Subtitles ใน Disney Plus เฉยๆผมขอแนะนำ Chrome Extention ที่พัฒนามาจากบทความนี้ให้ไปดาวน์โหลดกันได้เลยที่นี่ Disney Plus Multi Subtitles
โดยปกติแล้วพวกแพลตฟอร์มสตรีมมิ่งดูหนัง ดูซีรีย์ Disney+, Netflix หรือที่ไหนๆ จะสามารถเปิดซับไตเติ้ลได้แค่ภาษาเดียว แต่ทว่ามันก็จะมี Extention ที่เราไปสามารถโหลดมาใช้งานเพื่อเปิดได้ 2 ซับไตเติ้ล อย่างใน Netflix ผมก็จะใช้ eJOY AI Dictionary เอาไว้ฝึกภาษาซึ่งก็จะมีหลายเว็บเลยที่ extention ตัวนี้ซัพพอร์ท
แต่ว่ามันดันใช้งานไม่ได้ใน Disney+ คือมันสามารถเปิดได้ 2 ภาษานั้นแหละ แต่มันจะเอาภาษาหลักที่เราเลือก ไปแปลภาษาแทนไม่ได้ดึงซับมาจากหนังจริงๆ และพอดูไปสักพักตรงที่แปลภาษาก็จะโดนเบลอไว้ต้องเสียตัง ปกติก่อนหน้านี้ก็จะมี extention ตัวนึงที่ผมใช้อยู่ที่ชื่อว่า Dualsub แต่หลังจาก Disney+ อัพเดทหน้าเว็บก็ใช้งานไม่ได้ และเขาก็ไม่อัพเดทแล้วด้วย และพยายามหา extention อื่นๆแทนแต่ก็ไม่มีที่ใช้งานได้เลย นี่จึงเป็นที่มา ที่ผมต้องการทำ
โอเคมาเริ่มกันเลย ด้วยความที่ไม่รู้เลยว่าซับไตเติ้ลเนี่ย มันผลุบๆโผล่ๆ เปลี่ยนข้อความไปตามปากคนในหนังได้ยังไง ตอนนั้นก็ทำได้เพียงแค่ F12 Devtools เพื่อดูโค้ดว่าข้อความพวกนั้นมันขึ้นมาได้อย่างไร
ก็เจอว่าข้อความซับจะขึ้นอยู่ใน tag element นั้นที่จิ้มไฮไลท์ไว้ แล้วมันจะมาขึ้นตรงนี้ได้อย่างไรละ มันไปดึงหรือเอามาจากไหน ทิ่มไปเลยตรง tab network แล้วตรงหน้าเว็บก็ลองกดเปลี่ยนซับไตเติ้ลเป็นภาษาอื่นดู
จะเห็นได้ว่าพอกดเปลี่ยนซับก็จะมี request ใหม่ตัวนึงชื่อ subtitle.vtt เด้งขึ้นมามันคืออะไรไปดูกัน ก็อปปี้ลิงก์ url ไปเปิดดู ก็พอจะจับใจความได้ว่านี่มันคือซับไตเติ้ลทั้งเรื่องอยู่ในไฟล์นี้ ตัวเลขนั้นก็คงจะเป็น เวลาเริ่ม-จบ ของข้อความ
พอรู้ว่านี้คือไฟล์ที่เก็บซับไตเติ้ลทั้งหมดของเรื่อง แล้วยังไงต่อเอาตามตรง ตรงนี้ผมก็ช็อตไปหลายวันเพราะไม่รู้ว่าต้องทำอะไรต่อ จะเอาไฟล์มาใช้งานยังไง ช่วงนี้ก็จะเป็นการหาข้อมูล ไปลองๆคิดๆว่าแบบนี้ แบบนั้นมันทำได้มั้ยก็ไป ก็มีลองคิดว่าถ้าจะแทร็คเวลาจาก video ว่าตอนนี้ video นั่นเล่นอยู่วินาทีที่เท่าไหร่ ให้เอาข้อความไปแสดง
จนมาเจอว่า tag <video> เนี่ย ดูรูปภาพประกอบจากรูปที่ 2 ได้ ภายในตัวมันเองสามารถใส่ <track> ได้ รายละเอียด The Embed Text Track element ซึ่งมันก็ใช้ไฟล์ .vtt ข้างบนนี่เอง ซึ่งผมก็เจอตัวอย่างนี้มาลองเข้าไปดูได้ที่ Video subtitles with webvtt เดี๋ยวเรื่องผลุบๆโผล่ๆของข้อความมันจะจัดการให้เอง
ที่นี้ไหนเราลองเริ่มมาลงมือประกอบร่างพวก tag ต่างๆนั้นลงไปดูซิ ก็ไปที่หน้าเว็บเปิด DevTools ขึ้นมาไปที่ tag video ในส่วนนี้ให้ใส attribute เพิ่ม crossorigin=”anonymous” ลงในนั้น และใส่ track ลงไปภายในเปลี่ยน src ให้เรียบร้อยจากลิงก์ข้างบน
แท่แด๊… เราก็จะเห็นซับไตเติ้ลโผล่มาในรูปภาพอันล่างสุดคือที่เราเพิ่มเข้าไปเอง ส่วนอันบนนั้นเป็นของซับปกติ ทีนี้เราก็แค่ใส่ track เพิ่มเข้าไปอีกอันเป็นภาษาไทย ก็เห็นจาก url ก็พอเห็นความแตกต่างว่าน่าจะใช่
https://hses6.hotstar.com/videos/blaaaaa/subtitle/lang_eng_1690572338496/subtitle.vtt
https://hses6.hotstar.com/videos/blaaaaa/subtitle/lang_tha_1690572338496/subtitle.vtt
ทีนี้ก็โผล่มา 2 ซับแล้ว เวลาทีเราเปิดดูเต็มหน้าจอ ซับที่เราเพิ่มเองจะตัวใหญ่มากๆ และบางครั้งลำดับการแสดงซับก็จะมีเพี้ยนๆทุกครั้งที่ข้อความเปลี่ยน เดี๋ยวไทยอังกฤษ เดี๋ยวอังกฤษไทย ผมต้องการฟิกไว้เลยให้แสดงเป็น อังกฤษ-ไทย ซึ่งก็หาวิธีปรับไม่ได้เลย ไม่รู้ทำยังไง ก็งมไปอีกหลายวัน
จนมาสรุปหาวิธีได้ว่า เราก็ใช้รูปแบบ element ของซับหลักจาก disney+ เลย แล้วเดี๋ยวเราก็ดึงข้อความจากซับที่เราแอดเอง ไปแสดงตรงส่วนนั้นแทน ละเราก็ซ่อนการแสดงซับที่แอดเองไว้
จากรูปบน 2 บรรทัดบนคือ elemen ที่สร้างไว้ 2 อัน ทีนี้เราจะดึงข้อความจากข้างล่างนั่นแหละไปแทนข้างบน ซึ่งผมก็ไปเจอวิธีการคือการใช้ event cuechange ที่ผมจะทำอธิบายง่ายๆคือ ทุกๆครั้งที่ข้อความใน track เปลี่ยน ก็คือ 2 อันข้างล่าง ผมจะนำ text มันเองไปใส่ลงใน element ซับที่เราเพิ่มเอง ก็คือ 2 อันข้างบน แล้วเราก็ซ่อน track 2 อันล่างแทน ก็จะได้ดั่งรูปข้างล่างนี้เลย
ทีนี้จากข้างบนทั้งหมด ถ้าเรามานั่งเพิ่ม element นั่นแก้ element นี่ ทำใหม่ทุกครั้งก็คงจะเหนื่อยเกินไป เราก็เปลี่ยนขั้นตอนข้างบนทั้งหมดเป็นโค้ดก็จะได้
//url ลิงก์ของ subtitle.vtt
let subtitleUrlEN = "https://hses6.hotstar.com/videos/blaaaaa/subtitle/lang_eng_1690572338496/subtitle.vtt";
let subtitleUrlTH = "https://hses6.hotstar.com/videos/blaaaaa/subtitle/lang_tha_1690572338496/subtitle.vtt";
//เข้าถึง element video
let video = document.querySelector('video');
//เพิ่ม attribute ชื่อ crossorigin = anonymous ลงใน tag video
video.setAttribute("crossorigin", "anonymous");
//เพิ่ม tag track 2 อัน ลงใน element video
video.innerHTML = `<track src="${subtitleUrlEN}"></track><track src="${subtitleUrlTH}"></tracksrc>`;
//เข้าถึง element track ที่เราเพิ่มจากบรรทัดบน
let track = document.querySelectorAll("track");
//เปลี่ยน mode ของ track ทั้ง 2 ให้เป็น hidden คือมันจะทำงานอยู่เบื้องหลังแต่เราจะไม่เห็น
track[0].track.mode = "hidden";
track[1].track.mode = "hidden";
//เข้าถึง element ถัดไปจาก video ซึ่งจะได้ div ที่คลุมพวก subtitle
let txtSubElem = video.nextSibling;
//เปลี่ยน html ใน element div ทั้งหมดเป็นข้างล่างนี้ เป็นโค้ดจาก disney plus แค่เพิ่ม span เข้าไป 2 ชุดพร้อมใส่ id กำกับไว้
txtSubElem.innerHTML = `<div style="position: absolute; left: 0px; width: 100%; height: 100%; min-width: 48px; text-align: center; overflow: hidden; bottom: 0px; margin: auto;">
<span id="sub1" style="white-space: nowrap; padding: 0px 1rem; color: white; background-color: rgba(0, 0, 0, 0.8); direction: ltr; font-weight: 400; font-style: normal; left: 50%; transform: translateX(-50%); position: absolute; bottom: 9%; text-align: center; writing-mode: horizontal-tb;">SUB 1</span>
<span id="sub2" style="white-space: nowrap; padding: 0px 1rem; color: white; background-color: rgba(0, 0, 0, 0.8); direction: ltr; font-weight: 400; font-style: normal; left: 50%; transform: translateX(-50%); position: absolute; bottom: 5%; text-align: center; writing-mode: horizontal-tb;">SUB 2</span>
</div>`;
//เข้าถึง element id=sub1 จะได้ span มา
let sub1 = document.querySelector('#sub1');
//event cuechange คือเมื่อข้อความเปลี่ยน เราก็ดึงเอา text มันไปยัดลงใน span แทน
video.textTracks[0].oncuechange = function(e) {
let cue = this.activeCues[0];
if(cue){
sub1.innerHTML = cue.text;
}else{
sub1.innerHTML = '';
}
};
//เข้าถึง element id=sub2 จะได้ span มา
let sub2 = document.querySelector('#sub2');
//event cuechange คือเมื่อข้อความเปลี่ยน เราก็ดึงเอา text มันไปยัดลงใน span แทน
video.textTracks[1].oncuechange = function(e) {
let cue = this.activeCues[0];
if(cue){
sub2.innerHTML = cue.text;
}else{
sub2.innerHTML = '';
}
};
แต่จากโค้ดข้างบนเรายังคงต้องไปนั่งก็อปลิงก์ subtitle.vtt ทุกครั้งอยู่ดี จึงไปหาข้อมูลว่ามีวิธีไหนมั้ยที่จะดึงลิงก์นั้นมาได้เลย ก็ไปเจอการใช้ PerformanceResourceTiming ก็ก็อปๆโค้ดจากเว็บมันมาลองมั่วๆ เท่าที่เข้าใจคือ ถ้าหน้าเว็บมีการโหลด resource เพิ่มเติมมันก็จะเข้ามารันในนั้น
หลังจากนั้นเราก็แค่เช็คว่า resource ที่โหลดเข้ามาเนี่ยมันมีอันไหนที่มีคำ subtitle.vtt อยู่ในลิงก์มั้ย ถ้าเจอเราก็เอาลิงก์มันมาแล้วเอาไปใช้แทน ทีนี้เราก็ไม่ต้องมานั่งก็อปลิงก์เองแล้ว สรุปโค้ดก็จะได้เป็นอย่างนี้
//เข้าถึง element video
let video = document.querySelector('video');
//เพิ่ม attribute ชื่อ crossorigin = anonymous ลงใน tag video
video.setAttribute("crossorigin", "anonymous");
//เพิ่ม tag track 2 อัน ลงใน element video
video.innerHTML = "<track></track><track></track>";
//เข้าถึง element ถัดไปจาก video ซึ่งจะได้ div ที่คลุมพวก subtitle
let txtSubElem = video.nextSibling;
//เปลี่ยน html ใน element div ทั้งหมดเป็นข้างล่างนี้ เป็นโค้ดจาก disney plus แค่เพิ่ม span เข้าไป 2 ชุดพร้อมใส่ id กำกับไว้
txtSubElem.innerHTML = `<div style="position: absolute; left: 0px; width: 100%; height: 100%; min-width: 48px; text-align: center; overflow: hidden; bottom: 0px; margin: auto;">
<span id="sub1" style="white-space: nowrap; padding: 0px 1rem; color: white; background-color: rgba(0, 0, 0, 0.8); direction: ltr; font-weight: 400; font-style: normal; left: 50%; transform: translateX(-50%); position: absolute; bottom: 9%; text-align: center; writing-mode: horizontal-tb;">SUB 1</span>
<span id="sub2" style="white-space: nowrap; padding: 0px 1rem; color: white; background-color: rgba(0, 0, 0, 0.8); direction: ltr; font-weight: 400; font-style: normal; left: 50%; transform: translateX(-50%); position: absolute; bottom: 5%; text-align: center; writing-mode: horizontal-tb;">SUB 2</span>
</div>`;
//ถ้ามี resource โหลดใหม่ก็เข้ามารันในนี้
const observer = new PerformanceObserver((list) => {
list.getEntries().sort().forEach((entry) => {
//เช็คว่า resource ที่โหลดเข้ามาลิงก์มันมีคำว่า subtitle.vtt อยู่ไหม
if(entry.name.includes('subtitle.vtt')){
//ถ้าเจอ subtitle.vtt ให้หยุดการเช็ค resource ที่โหลดใหม่ๆเข้ามา
observer.disconnect();
//เก็บลิงก์ลงตัวแปร ตัวอย่างลิงก์ https://hses6.hotstar.com/videos/blaaaaa/subtitle/lang_eng_1690572338496/subtitle.vtt
let subtitleUrl = entry.name;
//regex เพื่อหาข้อความภาษาใน url
const regexp = /lang_(...)/g;
//ดึงข้อความ eng ออกมาจากลิงก์บางทีตอนเราโหลด resource อาจไปดึงซับภาษาอื่นมาเช่น อินโด(ind), มาเลย์(msa)
const languageSubtitle = [...subtitleUrl.matchAll(regexp)][0][1];
//เข้าถึง element track แล้วก็เอาลิงก์
let track = document.querySelectorAll("track");
//สร้างตัวแปรอยากให้ sub1, sub2 เป็นภาษาอะไร ดูจาก ISO 639-2 ว่า language code ใช้อะไร
let langCodeSub1 = "eng"; //อังกฤษ
let langCodeSub2 = "tha"; //ไทย
//replace จาก url ให้เป็นภาษาที่เราต้องการ แล้วก็ซ่อน track ไว้ไม่ให้แสดง
track[0].src = subtitleUrl.replace("/lang_"+languageSubtitle,"/lang_"+langCodeSub1);
track[0].track.mode = "hidden";
track[1].src = subtitleUrl.replace("/lang_"+languageSubtitle,"/lang_"+langCodeSub2);
track[1].track.mode = "hidden";
//เข้าถึง element id=sub1 จะได้ span มา
let sub1 = document.querySelector('#sub1');
//event cuechange คือเมื่อข้อความเปลี่ยน เราก็ดึงเอา text มันไปยัดลงใน span แทน
video.textTracks[0].oncuechange = function(e) {
let cue = this.activeCues[0];
if(cue){
sub1.innerHTML = cue.text;
}else{
sub1.innerHTML = '';
}
};
//เข้าถึง element id=sub2 จะได้ span มา
let sub2 = document.querySelector('#sub2');
//event cuechange คือเมื่อข้อความเปลี่ยน เราก็ดึงเอา text มันไปยัดลงใน span แทน
video.textTracks[1].oncuechange = function(e) {
let cue = this.activeCues[0];
if(cue){
sub2.innerHTML = cue.text;
}else{
sub2.innerHTML = '';
}
};
}
});
});
observer.observe({ type: "resource", buffered: true });
สามารถลองเอาไปรันดูได้ เปิด Devtools ขึ้นมา หรือกด F12 ก็ได้ จากนั้นไปที่ Console แล้ววางโค้ดชุดข้างบนลงไปเลย ก็จะได้ subtitles ขึ้นมา 2 อันคู่กันแล้ว