try on console add subtitle

เมื่อผมอยากทำ Multi Subtitles ใน Disney+ Hotstar ไว้ฝึกภาษา

ในบทความนี้จะมาสอนกระบวนการวิธีทำ Subtitles ให้มี 2 อันเปิดคู่กัน ไปจนสุดท้ายคือมีเขียนโค้ด Javascript อยู่ในส่วนท้ายบทความ

ส่วนถ้าใครหลงเข้ามาเพราะต้องการใช้งาน Multi Subtitles ใน Disney Plus เฉยๆผมขอแนะนำ Chrome Extention ที่พัฒนามาจากบทความนี้ให้ไปดาวน์โหลดกันได้เลยที่นี่ Disney Plus Multi Subtitles

โดยปกติแล้วพวกแพลตฟอร์มสตรีมมิ่งดูหนัง ดูซีรีย์ Disney+, Netflix หรือที่ไหนๆ จะสามารถเปิดซับไตเติ้ลได้แค่ภาษาเดียว แต่ทว่ามันก็จะมี Extention ที่เราไปสามารถโหลดมาใช้งานเพื่อเปิดได้ 2 ซับไตเติ้ล อย่างใน Netflix ผมก็จะใช้ eJOY AI Dictionary เอาไว้ฝึกภาษาซึ่งก็จะมีหลายเว็บเลยที่ extention ตัวนี้ซัพพอร์ท

ตัวอย่างรูปภาพ ซับไตเติ้ลใน netflix เมื่อใช้ extention ejoy

แต่ว่ามันดันใช้งานไม่ได้ใน Disney+ คือมันสามารถเปิดได้ 2 ภาษานั้นแหละ แต่มันจะเอาภาษาหลักที่เราเลือก ไปแปลภาษาแทนไม่ได้ดึงซับมาจากหนังจริงๆ และพอดูไปสักพักตรงที่แปลภาษาก็จะโดนเบลอไว้ต้องเสียตัง ปกติก่อนหน้านี้ก็จะมี extention ตัวนึงที่ผมใช้อยู่ที่ชื่อว่า Dualsub แต่หลังจาก Disney+ อัพเดทหน้าเว็บก็ใช้งานไม่ได้ และเขาก็ไม่อัพเดทแล้วด้วย และพยายามหา extention อื่นๆแทนแต่ก็ไม่มีที่ใช้งานได้เลย นี่จึงเป็นที่มา ที่ผมต้องการทำ

โอเคมาเริ่มกันเลย ด้วยความที่ไม่รู้เลยว่าซับไตเติ้ลเนี่ย มันผลุบๆโผล่ๆ เปลี่ยนข้อความไปตามปากคนในหนังได้ยังไง ตอนนั้นก็ทำได้เพียงแค่ F12 Devtools เพื่อดูโค้ดว่าข้อความพวกนั้นมันขึ้นมาได้อย่างไร

กด F12 เปิด Devtools เพื่อดู elementor ของ subtitle

ก็เจอว่าข้อความซับจะขึ้นอยู่ใน tag element นั้นที่จิ้มไฮไลท์ไว้ แล้วมันจะมาขึ้นตรงนี้ได้อย่างไรละ มันไปดึงหรือเอามาจากไหน ทิ่มไปเลยตรง tab network แล้วตรงหน้าเว็บก็ลองกดเปลี่ยนซับไตเติ้ลเป็นภาษาอื่นดู

tab network เพื่อดู request หลังจากกดเปลี่ยนซับ

จะเห็นได้ว่าพอกดเปลี่ยนซับก็จะมี request ใหม่ตัวนึงชื่อ subtitle.vtt เด้งขึ้นมามันคืออะไรไปดูกัน ก็อปปี้ลิงก์ url ไปเปิดดู ก็พอจะจับใจความได้ว่านี่มันคือซับไตเติ้ลทั้งเรื่องอยู่ในไฟล์นี้ ตัวเลขนั้นก็คงจะเป็น เวลาเริ่ม-จบ ของข้อความ

เปิดไฟล์ subtitle.vtt

พอรู้ว่านี้คือไฟล์ที่เก็บซับไตเติ้ลทั้งหมดของเรื่อง แล้วยังไงต่อเอาตามตรง ตรงนี้ผมก็ช็อตไปหลายวันเพราะไม่รู้ว่าต้องทำอะไรต่อ จะเอาไฟล์มาใช้งานยังไง ช่วงนี้ก็จะเป็นการหาข้อมูล ไปลองๆคิดๆว่าแบบนี้ แบบนั้นมันทำได้มั้ยก็ไป ก็มีลองคิดว่าถ้าจะแทร็คเวลาจาก 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 ให้เรียบร้อยจากลิงก์ข้างบน

add subtitle vtt eng

แท่แด๊… เราก็จะเห็นซับไตเติ้ลโผล่มาในรูปภาพอันล่างสุดคือที่เราเพิ่มเข้าไปเอง ส่วนอันบนนั้นเป็นของซับปกติ ทีนี้เราก็แค่ใส่ 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

add subtitle vtt eng 2

ทีนี้ก็โผล่มา 2 ซับแล้ว เวลาทีเราเปิดดูเต็มหน้าจอ ซับที่เราเพิ่มเองจะตัวใหญ่มากๆ และบางครั้งลำดับการแสดงซับก็จะมีเพี้ยนๆทุกครั้งที่ข้อความเปลี่ยน เดี๋ยวไทยอังกฤษ เดี๋ยวอังกฤษไทย ผมต้องการฟิกไว้เลยให้แสดงเป็น อังกฤษ-ไทย ซึ่งก็หาวิธีปรับไม่ได้เลย ไม่รู้ทำยังไง ก็งมไปอีกหลายวัน

จนมาสรุปหาวิธีได้ว่า เราก็ใช้รูปแบบ element ของซับหลักจาก disney+ เลย แล้วเดี๋ยวเราก็ดึงข้อความจากซับที่เราแอดเอง ไปแสดงตรงส่วนนั้นแทน ละเราก็ซ่อนการแสดงซับที่แอดเองไว้

add subtitle vtt eng 3

จากรูปบน 2 บรรทัดบนคือ elemen ที่สร้างไว้ 2 อัน ทีนี้เราจะดึงข้อความจากข้างล่างนั่นแหละไปแทนข้างบน ซึ่งผมก็ไปเจอวิธีการคือการใช้ event cuechange ที่ผมจะทำอธิบายง่ายๆคือ ทุกๆครั้งที่ข้อความใน track เปลี่ยน ก็คือ 2 อันข้างล่าง ผมจะนำ text มันเองไปใส่ลงใน element ซับที่เราเพิ่มเอง ก็คือ 2 อันข้างบน แล้วเราก็ซ่อน track 2 อันล่างแทน ก็จะได้ดั่งรูปข้างล่างนี้เลย

add subtitle vtt eng 4

ทีนี้จากข้างบนทั้งหมด ถ้าเรามานั่งเพิ่ม 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 อันคู่กันแล้ว

try on console add subtitle