YouTubeの動画では、シークバーの下にもっとも再生された部分を示す山型のグラフが表示されることがある。どこが多く再生されているかは波形でわかるが、具体的に何分何秒なのかまでは表示されない。このブックマークレットを使うと、このグラフのもとになっているデータから時刻を数値で抽出して、右パネルに一覧表示できる。

ブックマークレット

次のコードをブックマークのURL欄に設定する。

javascript:(function(){if(!location.href.includes('youtube.com/watch')){alert('YouTubeの動画ページで実行してください');return;}var o=document.getElementById('yt-mr-panel');if(o){o.remove();return;}var NOD='この動画には、もっとも再生された部分のデータがありません。';function findMarkers(d){var muts=d&&d.frameworkUpdates&&d.frameworkUpdates.entityBatchUpdate&&d.frameworkUpdates.entityBatchUpdate.mutations;if(!muts)return null;for(var i=0;i<muts.length;i++){var pl=muts[i].payload;if(pl&&pl.macroMarkersListEntity&&pl.macroMarkersListEntity.markersList){var ml=pl.macroMarkersListEntity.markersList;if(ml.markers&&ml.markers.length&&ml.markers[0].intensityScoreNormalized!==undefined)return ml.markers;}}return null;}var p=document.createElement('div');p.id='yt-mr-panel';p.style.cssText='position:fixed;top:60px;right:16px;width:320px;max-height:80vh;overflow-y:auto;background:#fff;border:1px solid #ddd;border-top:3px solid #f00;border-radius:4px;z-index:99999;padding:12px 16px;font-size:13px;font-family:sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.2)';var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;margin-bottom:10px';var ttl=document.createElement('span');ttl.style.cssText='color:#f00;font-weight:bold';ttl.textContent='もっとも再生された部分';var btns=document.createElement('div');var cp=document.createElement('button');cp.style.cssText='padding:3px 10px;background:#f00;color:#fff;border:none;border-radius:3px;cursor:pointer;font-size:12px;margin-right:6px;display:none';cp.textContent='コピー';var cl=document.createElement('button');cl.style.cssText='background:none;border:none;font-size:16px;cursor:pointer';cl.textContent='×';btns.appendChild(cp);btns.appendChild(cl);hdr.appendChild(ttl);hdr.appendChild(btns);p.appendChild(hdr);var rd=document.createElement('div');rd.textContent='読み込み中...';rd.style.color='#888';p.appendChild(rd);document.body.appendChild(p);cl.onclick=function(){p.remove();};function msg(t){rd.style.color='#888';rd.textContent=t;}function show(markers){var sum=0;markers.forEach(function(mk){sum+=Number(mk.intensityScoreNormalized);});var th=sum/markers.length*1.3;var groups=[],cur=null;markers.forEach(function(mk,idx){var score=Number(mk.intensityScoreNormalized),start=parseInt(mk.startMillis,10)/1000;if(score>=th){if(cur&&idx===cur.endIdx+1){cur.endIdx=idx;if(score>cur.maxScore){cur.maxScore=score;cur.peakStart=start;}}else{cur={endIdx:idx,peakStart:start,maxScore:score};groups.push(cur);}}});if(!groups.length){msg('該当する部分が見つかりませんでした。');return;}groups.sort(function(a,b){return b.maxScore-a.maxScore;});var top=groups.slice(0,10);var maxScore=top[0].maxScore;top.sort(function(a,b){return a.peakStart-b.peakStart;});function fmt(sec){sec=Math.floor(sec);var h=Math.floor(sec/3600),m=Math.floor(sec/60)-h*60,s=sec-h*3600-m*60;function p(n){return n<10?'0'+n:''+n;}return h>0?h+':'+p(m)+':'+p(s):m+':'+p(s);}var lt=top.map(function(g){return fmt(g.peakStart)+'(再生数の多さ '+Math.round(g.maxScore*100)+'%)';}).join('\n');ttl.textContent='もっとも再生された部分('+top.length+'件)';rd.textContent='';rd.style.color='';top.forEach(function(g){var pct=(g.maxScore/maxScore*100).toFixed(1);var bg='linear-gradient(to right,rgba(255,0,0,0.1) '+pct+'%,transparent '+pct+'%)';var bgHov='linear-gradient(to right,rgba(255,0,0,0.2) '+pct+'%,rgba(255,245,245,1) '+pct+'%)';var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;padding:5px 0;border-bottom:1px solid #f0f0f0;line-height:1.5;cursor:pointer;background:'+bg;row.onmouseover=function(){row.style.background=bgHov;};row.onmouseout=function(){row.style.background=bg;};row.onclick=function(){var vid=document.querySelector('video');if(vid)vid.currentTime=g.peakStart;};var ts=document.createElement('span');ts.style.cssText='color:#f00;font-weight:bold;flex-shrink:0;min-width:52px';ts.textContent=fmt(g.peakStart);var lb=document.createElement('span');lb.style.cssText='color:#333;flex:1';lb.textContent='再生数の多さ '+Math.round(g.maxScore*100)+'%';row.appendChild(ts);row.appendChild(lb);rd.appendChild(row);});cp.style.display='';cp.onclick=function(){navigator.clipboard.writeText(lt).then(function(){cp.textContent='コピーしました';setTimeout(function(){cp.textContent='コピー';},2000);}).catch(function(){cp.textContent='失敗';setTimeout(function(){cp.textContent='コピー';},2000);});};}var vid=new URLSearchParams(location.search).get('v');var d=window.ytInitialData;var dv=d&&d.currentVideoEndpoint&&d.currentVideoEndpoint.watchEndpoint&&d.currentVideoEndpoint.watchEndpoint.videoId;var mk=dv&&dv===vid?findMarkers(d):null;if(mk){show(mk);return;}fetch(location.href).then(function(r){return r.text();}).then(function(h){var parts=h.split('var ytInitialData = ');if(parts.length<2)parts=h.split('window["ytInitialData"] = ');var m2=null;if(parts.length>1){try{m2=findMarkers(JSON.parse(parts[1].split(';</script>')[0]));}catch(e){}}if(m2){show(m2);}else{msg(NOD);}}).catch(function(){msg('データを取得できませんでした。ページを再読み込みしてから試してください。');});})();

登録手順

  1. ブックマークバーを右クリックして、新しいブックマークを作る
  2. 名前を「もっとも再生された部分を抽出」など分かりやすいものにする
  3. URL欄に上のコードを貼り付けて保存する

使い方

  1. シークバーにもっとも再生された部分のグラフが表示されているYouTubeの動画ページを開く
  2. ブックマークレットをクリックする
  3. 右側にパネルが表示される。データ取得中は読み込み中と表示され、取得できると一覧に変わる(背景の塗りが広い行ほど再生数が多い)
  4. 行をクリックすると、その時間に動画がジャンプする
  5. コピーボタンで一覧をクリップボードに取得できる
  6. もう一度クリックするとパネルが閉じる
もっとも再生された部分の一覧パネルの表示例。背景の塗りが広い行ほど再生数が多い
一覧パネルの表示例。背景の塗りが広い行ほど再生数が多い

どう動いているか

YouTubeの動画ページには ytInitialData というオブジェクトが埋め込まれており、その中の frameworkUpdates.entityBatchUpdate.mutations に、もっとも再生された部分のグラフの元データである macroMarkersListEntity が含まれている。動画全体を100個前後の区間に分割し、各区間の開始時刻(startMillis)と再生数の多さ(intensityScoreNormalized、0〜1の数値)が記録されている。チャプター用のマーカーリストと混同しないよう、intensityScoreNormalized を持つものだけを拾う。

function findMarkers(data) {
  const mutations = data?.frameworkUpdates?.entityBatchUpdate?.mutations;
  if (!mutations) return null;

  for (const { payload } of mutations) {
    const markers = payload?.macroMarkersListEntity?.markersList?.markers;
    // intensityScoreNormalized を持つものだけが再生数のグラフのデータ
    if (markers?.length && markers[0].intensityScoreNormalized !== undefined) {
      return markers;
    }
  }
  return null;
}

ただしページ表示後の window.ytInitialData は当てにならない。YouTube内のリンクから動画を開いた場合は遷移前のページのデータのままで、直接開いた場合もアプリがデータを取り込む過程で frameworkUpdates が取り除かれることがある。そのため、URLの動画IDと window.ytInitialData 内の動画IDが一致し、かつマーカーが取れた場合だけその場で表示し、それ以外はページのHTMLを取得し直して埋め込まれた ytInitialData をパースする。

const videoId = new URLSearchParams(location.search).get('v');
const data = window.ytInitialData;
const dataVideoId = data?.currentVideoEndpoint?.watchEndpoint?.videoId;

// 動画IDが一致し、かつマーカーが取れた場合はその場で表示
const markers = (dataVideoId === videoId) ? findMarkers(data) : null;

if (markers) {
  show(markers);
} else {
  // それ以外はページのHTMLを取得し直して ytInitialData を読み直す
  fetch(location.href)
    .then((res) => res.text())
    .then((html) => {
      const json = html.split('var ytInitialData = ')[1]?.split(';</script>')[0];
      const reloaded = json && findMarkers(JSON.parse(json));
      reloaded ? show(reloaded) : msg('もっとも再生された部分のデータがありません。');
    });
}

マーカーが取得できたら、全区間の平均値の1.3倍をしきい値として、それを超える区間を抜き出す。隣り合う区間はひとつの山としてまとめ、山の中でもっとも再生数が多い区間の開始時刻を、その山の代表時刻として記録する。

// 全区間の平均値の1.3倍を「再生数が多い」のしきい値にする
let sum = 0;
markers.forEach((mk) => sum += Number(mk.intensityScoreNormalized));
const threshold = (sum / markers.length) * 1.3;

// しきい値を超える区間を、隣り合うものはひとつの山としてまとめる
const groups = [];
let current = null;
markers.forEach((mk, idx) => {
  const score = Number(mk.intensityScoreNormalized);
  const start = parseInt(mk.startMillis, 10) / 1000;
  if (score < threshold) return;

  if (current && idx === current.endIdx + 1) {
    // 直前の区間と連続している場合は同じ山として更新する
    current.endIdx = idx;
    if (score > current.maxScore) {
      current.maxScore = score;
      current.peakStart = start;
    }
  } else {
    // しきい値を超える新しい山の開始
    current = { endIdx: idx, peakStart: start, maxScore: score };
    groups.push(current);
  }
});

最後に再生数が多い順に並べて上位10件に絞り、時刻順に並べ直す。行をクリックすると video.currentTime をセットして動画をシークする。

// 再生数が多い順に並べて上位10件に絞り、時刻順に並べ直す
groups.sort((a, b) => b.maxScore - a.maxScore);
const top = groups.slice(0, 10);
top.sort((a, b) => a.peakStart - b.peakStart);

// 行をクリックしたら、その山の代表時刻に動画をシークする
row.onclick = () => {
  const video = document.querySelector('video');
  if (video) video.currentTime = group.peakStart;
};

注意

  • 再生数が少ない動画やライブ配信など、もっとも再生された部分のグラフ自体が存在しない動画では使えない。
  • 抽出される時刻は再生数が多い区間の代表値であり、厳密な秒数ではない。
  • データを取得し直す場合があるため、一覧の表示まで少し時間がかかることがある。
  • YouTubeのデータ構造が変わると動作しなくなる可能性がある。

関連記事

よくある質問

YouTubeでもっとも再生された部分の時刻を数値で知るには?
ブックマークレットを使うと、動画ページに埋め込まれたデータからもっとも再生された部分の時刻を一覧で取得できる。動画ページを開いてクリックするだけ。
グラフが表示されない動画でも使える?
再生数が少ない動画やライブ配信など、もっとも再生された部分のグラフ自体が存在しない動画では「もっとも再生された部分のデータがありません」と表示される。
コメントのタイムスタンプ抽出と何が違う?
こちらは再生数の集計データを使うため、コメント欄の内容に左右されない。コメントが少ない動画や、コメント欄でチャプターがまとめられていない動画でも使える。