画面外字幕のスクリプト

Firefox系の縦揺れに対処

ハイライト:

  • なぜ画面外字幕を作ったか

  • 去年まで使っていたoutscreen-subtitles.jsの問題

  • outscreen-subtitles-2024.jsの詳細

なぜ画面外字幕を作ったか

このサイトの特徴は、英語動画の和訳字幕が画面外に表示されることです。

もちろんプレーヤーに備わるCCボタンと歯車ボタンによって、動画にオーバーラップして英語または日本語の字幕を本来の形で表示することも可能ですが、ある程度まとまった文章が画面外に表示されていれば、内容が把握しやすいのではないかと思ってスクリプトで機能を追加しました。図表などの画面の時、字幕で隠れて解りづらいということもなく、重宝しています。

また、テキストなのでコピー・ペーストが簡単にでき、検索や拡散にも役立つだろうという思いもあります。

去年まで使っていたoutscreen-subtitles.jsの問題

ネットで見つけた"outscreen-subtitles.js"を自サイト用に調整して、ほぼ満足できる形で運用してきましたがいくつか問題がありました。

  • jQueryの古いバージョンに依存しているので、複数のjQueryを置かなければならなかった。

  • オリジナルのサイトが見つからない。どこでどう見つけたのか皆目わからず、サーチしても関連情報が見つけられない。

  • Chrome系のブラウザでは問題ないが、Firefox系だと動画が進むにつれてschroll-topが怪しくなり、字幕が切り替わるたびに縦に揺れるような表示になってしまう。

  • ページを何回か再読込しないと、和訳字幕が表示されないことがわりと頻繁にある。

outscreen-subtitles-2024.jsの詳細

上記の問題を解決するため、オリジナルの中身を読んでスクリプトを書き直すことにしました。

以下にhtml, css, jsのコードを掲載します。画面外字幕の需要があるかどうか分かりませんが、誰かの参考になればと思います。

html

動画プレーヤーPlyrのcssとライブラリは既に読み込ませてあります。ここには動画再生と字幕に関係のある箇所だけ載せています。

<div style="margin-bottom: 4px">
    <video class="video-today" id="videoToday" controls data-poster="●">
        <source src="●" type="video/mp4">
        <track id="etrack" label="English" kind="subtitles" src="/asset/2024/●/Jimaku.vtt" srclang="en">
        <track id="jtrack" label="和訳" kind="subtitles" src="/asset/2024/●/Jimaku2.vtt" srclang="jp" default>
    </video>
</div>
<div id="display-cues-1" class="display-cues" style="margin-top: -20px !important;">
</div>
<script>
    // Plyrインスタンス
    const player = new Plyr("#videoToday", {
        hideControls: true,
        displayDuration: true,
        invertTime: false,
        captions: { language: "en", active: true, update: true }
        });
    player.volume = 0.3;
</script>
<script src="/js/outscreen-subtitles-2024.js"></script>

css

/* 画面外字幕 */
.display-cues {
    width: 100%;
    height: 160px !important;
    background: whitesmoke;
    padding: 0px 10px !important;
    overflow-y: scroll;
    text-align: center;
    color: gray;
    border-style: solid;
    border-width: 0px 1px 1px 1px !important;
    border-color: #dddddd;
    outline: none;
}

.display-cues p {
    line-height: 20px !important;
    font-size: 16px !important;
    letter-spacing: 1px !important;
    margin: 0px 0px 20px 0px !important;
    word-break: break-all;
    cursor: pointer;
}

outscreen-subtitles-2024.js

// 字幕トラックの読取りと画面下に表示
// https://developer.mozilla.org/en-US/docs/Web/API/TextTrack/mode
/* 2024.01.03
 * Firefox系のブラウザで起こる縦揺れを避けるために、jQueryを使わないように書き直した
 */

// display-cues-1にメッセージを表示
document.getElementById('display-cues-1').innerHTML = '<div style="padding-top: 20px">* * * 読込中... * * *</div>'

// ページ読込完了を待って実行
// 動画の進行、あるいは離れたキューのクリックによって
// 話されている訳文が字幕コンテナのトップに表示されるようにする
window.onload = function () {
    // vttデータからタイムコードとテキストを抽出するための準備
    const video = document.querySelector("video")
    const jContainer = document.getElementById('display-cues-1')
    const track = document.getElementById('jtrack').track
    track.mode = "hidden"
    const cues = track.cues
    const dummyEnd = cues[0].startTime

    // 最初の固定字幕
    let cuesTime = [[0,dummyEnd]]
    let cuesText = ['<p class="cue-active">* * * 開始... * * *']

    // cuesからデータをプッシュ
    for ( var i=0; i<cues.length; i++) {
        cuesTime.push([cues[i].startTime, cues[i].endTime])
        cuesText.push(cues[i].text)
    }

    // 最後の固定字幕
    const endIndx = cues.length - 1
    cuesTime.push([cues[endIndx].endTime, video.duration])
    cuesText.push('<p>* * * 字幕ここまで * * *')

    // それぞれの<p>に属性をつけるための準備
    let tStart, tEnd, txt, txtPrev, Tx, txtTagRmvd, txLen
    let cueP = ''
    let scAmtAccum = -20
    let scAmt = 0

    // jContainerの幅によって1行に表示できる文字数は可変なので最初に計算
    const jConW = jContainer.clientWidth - 22     // 左右のパディングとボーダー分
    const charPerLine = parseInt(jConW / (16+1))    // フォントサイズと字間

    // 本来の文字数と半角は0.5文字として数えたもの
    const mojiCount = function (jStr) {   //無名関数
        let len0 = jStr.length
        let len = 0
        for ( j=0; j < jStr.length; j++) {
            if ( jStr[j].match(/[ -~]/) ) {
                len += 0.5
            } else {
                len += 1
            }
        }
        return [len0, len]
    }

    // タイムコードとスクロール量を入れて表示用文字列を作成
    for (var i = 0; i < cuesTime.length; i++) {
        tStart = cuesTime[i][0]
        tEnd = cuesTime[i][1]
        txt = cuesText[i]
        if (i==0) {
            txtPrev = ''
        } else {
            txtPrev = cuesText[i-1]
        }

        // htmlのタグ部分を除去
        txtTagRmvd = txtPrev.replace(/(<([^>]+)>)/gi, '')

        // 本来の文字数と半角混じりの数
        txLen0 = mojiCount(txtTagRmvd)[0]
        txLen = mojiCount(txtTagRmvd)[1]

        // 現行字幕をコンテナトップに表示するためのスクロール位置の計算
        let joyo = txLen % charPerLine
        let reqdGyo = parseInt(txLen/charPerLine)+1
        // 最初にデフォルトの計算結果を代入
        scAmt = (reqdGyo)*20 + 20
        // 特殊な場合① 余りがゼロ
        if ( joyo==0 ) {
            scAmt = reqdGyo*20
        }
        // 特殊な場合② 本来の文字数との差が0.5ではみ出し数も0.5
        if ( (txLen - (reqdGyo-1)*charPerLine == 0.5) && (txLen0 - txLen) == 0.5) {
//console.log("前の文字数:", txLen, 'スク量:',scAmt, txt)
            scAmt = reqdGyo*20
        }

        // その時間帯に入った時のスクロール量は個々のスクロール量の累計
        scAmtAccum = scAmtAccum + scAmt

        //cuesTimeの要素にスクロール位置を追加
        cuesTime[i].push(scAmtAccum)

        // コンテナに設置するパラグラフ
        Tx = '<p id="q' + i +
             '" onclick="jumpToTime(' +
              tStart + ',' +
              scAmtAccum +
             ', q' + i +
             ')">' + txt + '</p>'
        // ひとつながりの文字列を作成
        cueP = cueP + Tx
    }
    // 字幕文字列をコンテナに収納
    jContainer.innerHTML = cueP

    // 動画の進行につれてスクロール、現行の字幕が動画直下に来る
    let jiStart, jiEnd, jiPrev, vCurrent, qid, qCurrent
    jiPrev = 0
    video.ontimeupdate = function() {
        vCurrent = video.currentTime
        imaDoko(vCurrent)
        function imaDoko(vCurrent) {
            for (var t = 0; t < cuesTime.length; t++) {
                jiStart = cuesTime[t][0]
                jiEnd = cuesTime[t][1]
                if ( vCurrent > jiStart && vCurrent < jiEnd ) {
                    if (jiPrev != jiStart) {
                        // 全部のcueをチェックしてアクティブを外す
                        removeActive()
                        // 次のキューをトップに
                        jContainer.scrollTo(0, cuesTime[t][2])
                        // このキューにアクティブをマーク
                        qid = "q"+t
                        qCurrent = document.getElementById(qid)
                        qCurrent.classList.add("cue-active")
                        jiPrev = jiStart
                        break
                    }
                }
            }
        }
    }

    // クリックで飛んだところから再生
    jumpToTime = function (t, pos, qid) {
        // 全部のcueをチェックしてアクティブを外す
        removeActive()
        // クリックされた行をコンテナのトップに
        video.currentTime = t
        jContainer.scrollTo(0, pos)
        // アクティブクラスにする
        qid.classList.add("cue-active")
    }

    // 既存のcue-activeクラスを外す
    function removeActive() {
        for (var z=0; z < cuesTime.length; z++) {
            let q = "q"+z
            let Q = document.getElementById(q)
            if (Q.classList.contains("cue-active")) {
                Q.classList.remove("cue-active")
            }
        }
    }

    // ビデオのバッファリングが終わったらリロードしてコンテナに字幕を表示
    video.canplaythrough = function() {
        window.location.reload()
    }
}


関連記事